https://kotlinlang.org logo
Title
s

s3rius

03/23/2023, 6:57 PM
Hey there, is there a way to insert an observer at the point where a ChildStack animation finishes? My scenario is as follows: I'm trying to write a few integration tests that run the whole app, execute a few UI commands to progress the UI, and assert that everything works reasonably well. To improve this, I want to create an automatism that takes a screenshot of the current compose scene after every navigation. If a test fails, I can present the user with a set of screenshots to help them identify where things went wrong. I can observe ChildStack's current value, but at that point the transition animation hasn't happened yet, and I basically take a screenshot of the previous scene. Here is an example of how the test currently looks like, with me manually calling
awaitIdle()
before taking a screenshot. This works because it waits until the animation has finished, but since
awaitIdle()
is a suspend function, I cannot just run it in a ChildStack observer.
@Test
fun test() = runBlocking {
    var count = 0

    // The user flow is: Login -> Onboarding -> Dashboard
    val main = MainComponent(testContext(lifecycleRegistry))

    rule.setContentAndAwaitIdle {
        MainUi(main)
    }
    lifecycleRegistry.start()
    
    rule.awaitIdle()
    rule.renderToFile("${count++}.jpg") // Takes a screenshot of the Login screen

    // Perform login
    rule.onNodeWithText("E-Mail").performTextInput("test")
    rule.onNodeWithText("Password").performTextInput("word")
    rule.onNode(hasText("Login").and(hasClickAction().and(isEnabled()))).performClick()

    rule.awaitIdle()
    rule.renderToFile("${count++}.jpg") // Takes a screenshot of the Onboarding screen

    rule.onNode(hasClickAction()).performClick() // Accept Onboarding

    rule.awaitIdle()
    rule.renderToFile("${count++}.jpg") // Takes a screenshot of the Dashboard screen
}
If I want to automate the process of taking screenshots the only options I can see are (1) observe the moment the transition animation has ended or (2) modify the code so that no animation happens during tests (but even then I think I'm actually taking a screenshot of the frame before the new scene is rendered).
a

Arkadii Ivanov

03/23/2023, 8:25 PM
Hello! When the animation finishes, the previous Composable gets destroyed. Perhaps, you could do the following?
Children(
        stack = childStack,
        modifier = Modifier.weight(weight = 1F),
        animation = tabAnimation(),
    ) {
        when (val child = it.instance) {
            is CountersChild -> CountersContent(component = child.component, modifier = Modifier.fillMaxSize())
            is MultiPaneChild -> MultiPaneContent(component = child.component, modifier = Modifier.fillMaxSize())
            is DynamicFeaturesChild -> DynamicFeaturesContent(component = child.component, modifier = Modifier.fillMaxSize())
            is CustomNavigationChild -> CustomNavigationContent(component = child.component, modifier.fillMaxSize())
        }

        DisposableEffect(it.configuration) {
            onDispose {
                // The animation is finished
            }
        }
    }
You can try also waiting for
idle
in tests. This is how I tested the
Children
function with animations - link.
s

s3rius

03/23/2023, 9:14 PM
Hey, thanks for the reply! I'll try the DisposableEffect and see if it works. Waiting for idle is kind-of iffy. It works if I integrate it into the test, as in my example. But best-case I don't want to care about the screenshot mechanic at all inside a test; it should "just work". But explicitly waiting for idle has to be woven into the flow of the test. Or at least that is my understanding.
a

Arkadii Ivanov

03/23/2023, 9:29 PM
Waiting for idle in UI and integration tests (for animations and background processing) is a common practice, based on my experience.
But feel free to try other approaches 😀