8

We seem to have an issue with AndroidComposeRule, we have a simple test with waitUntil {} function:

@RunWith(AndroidJUnit4::class)
class IdenfyFaceReauthenticationFlowTests {

  @get:Rule
  val composeTestRule = createAndroidComposeRule<FaceReauthenticationActivity>()

  private lateinit var faceReauthenticationActivity: FaceReauthenticationActivity

  @Before
  fun setUp() {
    faceReauthenticationActivity = composeTestRule.activity
    faceReauthenticationActivity.idenfyMainViewModel.idenfyInternalSettings.isInitialViewClosed = false
  }

  @Test
  fun test1() {
    composeTestRule.waitUntil(8000) {
      composeTestRule.onAllNodesWithText(faceReauthenticationActivity.getString(R.string.idenfy_camera_onboarding_view_continue_button_title_v2)).fetchSemanticsNodes(false).isNotEmpty()
    }
    composeTestRule.onNodeWithText(faceReauthenticationActivity.getString(R.string.idenfy_camera_onboarding_view_continue_button_title_v2)).performClick()

    Thread.sleep(6000)

    //Face Reauthentication Results View V2
    IdenfyView().waitForView(ViewMatchers.withId(R.id.idenfy_button_face_reauthentication_results_continue)).perform(
      ViewActions.click())

    Thread.sleep(1000)

    val activityResult = composeTestRule.activityRule.scenario.result!!
    activityResult.resultData.setExtrasClassLoader(this::class.java.classLoader)
    val faceReauthenticationResult: FaceReauthenticationResult =
      activityResult.resultData.getParcelableExtra(IdenfyController.IDENFY_FACE_REAUTHENTICATION_RESULT)!!

    assert(faceReauthenticationResult.faceReauthenticationStatus == FaceReauthenticationStatus.SUCCESS)
  }

  @After
  fun tearDown() {
    composeTestRule.activityRule.scenario.close()
  }
}

The problem that we are facing, is that SOMETIMES the test fails with the following error:

androidx.compose.ui.test.junit4.android.ComposeNotIdleException: Idling resource timed out: possibly due to compose being busy.
IdlingResourceRegistry has the following idling resources registered:
- [busy] androidx.compose.ui.test.junit4.android.ComposeIdlingResource@df5cec9 
All registered idling resources: Compose-Espresso link
    at androidx.compose.ui.test.junit4.android.EspressoLink_androidKt.rethrowWithMoreInfo(EspressoLink.android.kt:135)
    at androidx.compose.ui.test.junit4.android.EspressoLink_androidKt.runEspressoOnIdle(EspressoLink.android.kt:109)
    at androidx.compose.ui.test.junit4.android.EspressoLink.runUntilIdle(EspressoLink.android.kt:78)
    at androidx.compose.ui.test.junit4.AndroidComposeTestRule.waitForIdle(AndroidComposeTestRule.android.kt:289)
    at androidx.compose.ui.test.junit4.AndroidComposeTestRule.access$waitForIdle(AndroidComposeTestRule.android.kt:155)
    at androidx.compose.ui.test.junit4.AndroidComposeTestRule$AndroidTestOwner.getRoots(AndroidComposeTestRule.android.kt:441)
    at androidx.compose.ui.test.TestContext.getAllSemanticsNodes$ui_test_release(TestOwner.kt:95)
    at androidx.compose.ui.test.SemanticsNodeInteractionCollection.fetchSemanticsNodes(SemanticsNodeInteraction.kt:234)
    at androidx.compose.ui.test.SemanticsNodeInteractionCollection.fetchSemanticsNodes$default(SemanticsNodeInteraction.kt:227)
    at com.idenfy.idenfySdk.flowtests.IdenfyFaceReauthenticationFlowTests$reauthenticationOldStatusReturned_failedStatusReturned$1.invoke(IdenfyFaceReauthenticationFlowTests.kt:145)
    at com.idenfy.idenfySdk.flowtests.IdenfyFaceReauthenticationFlowTests$reauthenticationOldStatusReturned_failedStatusReturned$1.invoke(IdenfyFaceReauthenticationFlowTests.kt:144)
    at androidx.compose.ui.test.junit4.AndroidComposeTestRule.waitUntil(AndroidComposeTestRule.android.kt:317)
    at com.idenfy.idenfySdk.flowtests.IdenfyFaceReauthenticationFlowTests.reauthenticationOldStatusReturned_failedStatusReturned(IdenfyFaceReauthenticationFlowTests.kt:144)
    at java.lang.reflect.Method.invoke(Native Method)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at androidx.test.internal.runner.junit4.statement.RunBefores.evaluate(RunBefores.java:80)
    at androidx.test.internal.runner.junit4.statement.RunAfters.evaluate(RunAfters.java:61)
    at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:48)
    at androidx.compose.ui.test.junit4.AndroidComposeTestRule$AndroidComposeStatement.evaluateInner(AndroidComposeTestRule.android.kt:357)
    at androidx.compose.ui.test.junit4.AndroidComposeTestRule$AndroidComposeStatement.evaluate(AndroidComposeTestRule.android.kt:346)
    at androidx.compose.ui.test.junit4.android.EspressoLink$getStatementFor$1.evaluate(EspressoLink.android.kt:63)
    at androidx.compose.ui.test.junit4.IdlingResourceRegistry$getStatementFor$1.evaluate(IdlingResourceRegistry.jvm.kt:160)
    at androidx.compose.ui.test.junit4.android.ComposeRootRegistry$getStatementFor$1.evaluate(ComposeRootRegistry.android.kt:150)
    at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
    at org.junit.rules.RunRules.evaluate(RunRules.java:20)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at androidx.test.ext.junit.runners.AndroidJUnit4.run(AndroidJUnit4.java:154)
    at org.junit.runners.Suite.runChild(Suite.java:128)
    at org.junit.runners.Suite.runChild(Suite.java:27)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
    at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
    at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:395)
    at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2252)
Caused by: androidx.test.espresso.IdlingResourceTimeoutException: Wait for [Compose-Espresso link] to become idle timed out
    at androidx.test.espresso.IdlingPolicy.handleTimeout(IdlingPolicy.java:16)
    at androidx.test.espresso.base.UiControllerImpl$5.resourcesHaveTimedOut(UiControllerImpl.java:4)
    at androidx.test.espresso.base.IdlingResourceRegistry$Dispatcher.handleTimeout(IdlingResourceRegistry.java:44)
    at androidx.test.espresso.base.IdlingResourceRegistry$Dispatcher.handleMessage(IdlingResourceRegistry.java:12)
    at android.os.Handler.dispatchMessage(Handler.java:108)
    at androidx.test.espresso.base.Interrogator.loopAndInterrogate(Interrogator.java:53)
    at androidx.test.espresso.base.UiControllerImpl.loopUntil(UiControllerImpl.java:155)
    at androidx.test.espresso.base.UiControllerImpl.loopMainThreadUntilIdle(UiControllerImpl.java:129)
    at androidx.test.espresso.Espresso$1.run(Espresso.java:2)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:458)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at android.os.Handler.handleCallback(Handler.java:907)
    at android.os.Handler.dispatchMessage(Handler.java:105)
    at android.os.Looper.loop(Looper.java:216)
    at android.app.ActivityThread.main(ActivityThread.java:7625)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:524)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:987)

The test seems to get stuck at this line (The composable is definitely visible):

   composeTestRule.waitUntil(8000) {
      composeTestRule.onAllNodesWithText(faceReauthenticationActivity.getString(R.string.idenfy_camera_onboarding_view_continue_button_title_v2)).fetchSemanticsNodes(false).isNotEmpty()
    }

Our dependencies (With 1.0.5 compose version):
//Compose
implementation “androidx.compose.ui:ui:$compose_version”
implementation “androidx.compose.material:material:$compose_version”
implementation “androidx.compose.ui:ui-tooling-preview:$compose_version”
implementation ‘androidx.lifecycle:lifecycle-runtime-ktx:2.4.0’
implementation ‘androidx.activity:activity-compose:1.4.0’
//Compose testing
debugImplementation “androidx.compose.ui:ui-test-manifest:1.0.5"

Is something wrong in our implementation?

EDIT

After some more tries, we discovered the cause of this problem, we have two mutable states:

val currentInstructionDescription: MutableState<String> = remember {
  mutableStateOf("")
}

val progress = remember {
  mutableStateOf(0.0f)
}

These two states are changing all the time in the view (every second), one of them changes the progress of a progress bar, another updates a text:

LaunchedEffect(key1 = Unit, block = {
  val timer = (0..Int.MAX_VALUE)
    .asSequence()
    .asFlow()
    .onEach { delay(1000) }

  timer.collect {
    progress.value = progressValueInFloat // increasing value
    currentInstructionDescription.value = "Custom String"
  }
})

Having this code disabled, the tests run fine and idling resources get idled. Maybe there is something wrong with our implementation of this code?

Viktor Vostrikov
  • 1,322
  • 3
  • 19
  • 36
  • I think, since your UI is still working there is nothing strange in espresso timout. Have you tried to use test dispatcher to manualy manage delay time in test? – Karol Kulbaka May 12 '22 at 05:07

1 Answers1

0

I had similar issue. My problem was on how I was retrieving CoroutineScope for the test:

@get:Rule()
var coroutineRule = MainDispatcherRule() // this caused the issue

I got the MainDispatcherRule implementation from Testing Kotlin coroutines on Android, but it caused the problem:

// Reusable JUnit4 TestRule to override the Main dispatcher
class MainDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

Soln:

get the coroutineScope the following way as shown in this post:

val coroutineScope = CoroutineScope(Dispatchers.Unconfined)
leo
  • 113
  • 3
  • 11