0

I'm trying to make a compose app with a different top app bar per screen. My strategy is to pass down a state setter for the data of the app bar through each screen. However passing in an onClick lambda is breaking my tests.

Here's an SSCCE:

// ActionState.kt
class ActionState(val title: String = "", val onClick: () -> Unit = {}) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as ActionState

        if (title != other.title) return false
        // Commenting out this line fixes the test but breaks the implementation
        if (onClick != other.onClick) return false

        return true
    }

    override fun hashCode(): Int {
        var result = title.hashCode()
        result = 31 * result + onClick.hashCode()
        return result
    }
}

// MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            App()
        }
    }
}

@Composable
fun TopBar(actionState: ActionState) {
    TopAppBar(
        title = { Text("App") },
        actions = {
            val contentColor = contentColorFor(MaterialTheme.colors.primarySurface)
            val colors = ButtonDefaults.textButtonColors(contentColor = contentColor)
            TextButton(onClick = actionState.onClick, colors = colors) {
                Text(actionState.title)
            }
        },
    )
}

@Composable
fun ScreenA(setActionState: (ActionState) -> Unit, navigateToScreenB: () -> Unit) {
    setActionState(ActionState("To B", navigateToScreenB))
    Text("Screen A")
}

@Composable
fun ScreenB(setActionState: (ActionState) -> Unit, navigateToScreenA: () -> Unit) {
    setActionState(ActionState("To A", navigateToScreenA))
    Text("Screen B")
}

@Composable
fun App() {
    val navController = rememberNavController()
    val (actionState, setActionState) = remember { mutableStateOf(ActionState()) }
    Scaffold(topBar = { TopBar(actionState = actionState) }) {
        NavHost(navController = navController, startDestination = "a") {
            composable("a") {
                ScreenA(
                    setActionState = setActionState,
                    navigateToScreenB = { navController.navigate("b") },
                )
            }
            composable("b") {
                ScreenB(
                    setActionState = setActionState,
                    navigateToScreenA = { navController.navigate("a") },
                )
            }
        }
    }
}

// Test
class ExampleInstrumentedTest {
    @get:Rule
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    @Test
    fun test() {
        composeTestRule.onNodeWithText("Screen A").assertIsDisplayed()
        composeTestRule.onNodeWithText("To B").performClick()
        composeTestRule.onNodeWithText("Screen B").assertIsDisplayed()
    }
}

The error I'm getting in the test is:

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@408168
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.SemanticsNodeInteraction.fetchSemanticsNodes$ui_test_release(SemanticsNodeInteraction.kt:79)
at androidx.compose.ui.test.SemanticsNodeInteraction.fetchOneOrDie(SemanticsNodeInteraction.kt:145)
at androidx.compose.ui.test.SemanticsNodeInteraction.fetchSemanticsNode(SemanticsNodeInteraction.kt:96)
at androidx.compose.ui.test.AndroidAssertions_androidKt.checkIsDisplayed(AndroidAssertions.android.kt:29)
at androidx.compose.ui.test.AssertionsKt.assertIsDisplayed(Assertions.kt:33)
at ogbe.eva.topbarbug.ExampleInstrumentedTest.test(ExampleInstrumentedTest.kt:16)
... 33 trimmed
Caused by: androidx.test.espresso.IdlingResourceTimeoutException: Wait for [Compose-Espresso link] to become idle timed out
at androidx.test.espresso.IdlingPolicy.handleTimeout(IdlingPolicy.java:4)
at androidx.test.espresso.base.UiControllerImpl$5.resourcesHaveTimedOut(UiControllerImpl.java:1)
at androidx.test.espresso.base.IdlingResourceRegistry$Dispatcher.handleTimeout(IdlingResourceRegistry.java:4)
at androidx.test.espresso.base.IdlingResourceRegistry$Dispatcher.handleMessage(IdlingResourceRegistry.java:6)
at android.os.Handler.dispatchMessage(Handler.java:102)
at androidx.test.espresso.base.Interrogator.loopAndInterrogate(Interrogator.java:14)
at androidx.test.espresso.base.UiControllerImpl.loopUntil(UiControllerImpl.java:8)
at androidx.test.espresso.base.UiControllerImpl.loopMainThreadUntilIdle(UiControllerImpl.java:17)
at androidx.test.espresso.Espresso$1.run(Espresso.java:1)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:462)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7839)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

If I remove the onClick lambda from the equality test then the test passes. But then the action doesn't work in the app when I navigate away from the screen and navigate back. I think it ends up using stale data.

I'm thinking the problem has to do with the equality check in either remember or mutableStateOf but I don't know enough about those methods to really understand what's going on.

How do I prevent the tests from idling? Alternatively, is there a better way to implement per screen top app bar state?

1 Answer 1

0

I moved the scaffold into each separate screen. I haven't noticed the top app bar flickering, so they might have fixed the bug that used to cause that.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.