Harald
Harald

Reputation: 81

how do I run a Jetpack Compose test in realtime?

Using Jetpack Compose on Android.

I have a test, that simulates a selection of several Text composable in a Column.

The selection starts by a long-press on the first item and then moves down over more Text composables and stop well inside the Column.

Usually the test should run unattended and fast. But I want to be able to show the selection process in real time (for demonstration purposes and also to see, if it works like it's designed, e.g. at the beginning I forgot that I have to wait some time after the down()).

The first Text composable in the column is also used to find the element (->anchor), and the parent is the Column which is used to perform the move.

This is the function that performs the selection:

val duration = 3000L
val durationLongPress = 1000L

fun selectVertical(anchor: SemanticsNodeInteraction, parent: SemanticsNodeInteraction) {
    anchor.performTouchInput {
        down(center)
    }

    clock.advanceTimeBy(durationLongPress)
      // the time jumps here, but the selection of the first word is visible

    val nSteps = 100
    val timeStep = (duration-durationLongPress)/nSteps
    parent.performTouchInput {
        moveTo(topCenter)
        val step = (bottomCenter-topCenter)*0.8f/ nSteps.toFloat()
        repeat(nSteps) {
            moveBy(step, timeStep)
        }
        up()
    }
}

this is the composable:

@Composable
fun SelectableText() {
    val text = """
        |Line
        |Line start selecting here and swipe over the empty lines
        |Line or select a word and extend it over the empty lines
        |Line
        |
        |
        |
        |Line
        |Line
        |Line
        |Line
        """.trimMargin()
    Column {
        SelectionContainer {
            Column {
                Text("simple")
                Text(text = text)   // works
            }
        }
        SelectionContainer {
            Column {
                Text("crash")
                text.lines().forEach {
                    Text(text = it)
                }
            }
        }
        SelectionContainer {
            Column {
                Text("space")
                text.lines().forEach {
                    // empty lines replaced by a space works
                    Text(text = if (it == "") " " else it)
                }
            }
        }
    }
}

a test goes like this:

@Test
fun works_simple() {
    val anchor = test.onNodeWithText("simple")
    val textNode = anchor.onParent()
    textNode.printToLog("simple")
    controlTime(duration) {
        selectVertical(anchor, textNode)
    }
}

controlTime is the part that does not work. I don't add it's code here to keep the solution open.

I tried to disable the autoAdvance on the virtual test clock and stepping the time in a loop in a coroutine.

When I step the time in 1ms steps and add a delay(1) each, the wait is correct, but I don't see the selection expanding (I want at least see the steps). Instead I see the selection of the first word, then nothing until the end of the move and then the end result.

I also divided the move into smaller steps e.g. 10 or 100, but it's still not showing the result.

Upvotes: 0

Views: 890

Answers (1)

Harald
Harald

Reputation: 81

ok, I found the solution myself when sleeping... the "sleeping" brain is obviously working on unsolved problems (well, I know this already).

The key is, to do each move that should be visible in it's own performXXX. I think, the result is only propagated to the UI, when the code block is finished, which makes sense for a test.

parent.performTouchInput {
    inRealTime("moveBy($step, $timeStep)", timeStep) {
        moveBy(step)
    }
}

I couldn't find a way to determine the duration of a so called "frame", so I advance either the virtual or the real clock, depending on which is lagging until both reach the target time. This can probably be optimized to jump both clocks in one step. I'll investigate that later.

It's interesting, that even 100 steps don't show a smooth selection move. Instead, there are still only a few steps, even when the step time is increased.

Btw. this purpose of this code is to show a crash in SelectionContainer, when it encounters an empty Text("") composable for a bug report I created. I will provide it on the issue tracker, but I also want to have the test in our app development, to see, when it's solved and to avoid a library that doesn't work. Sometimes we encounter regressions in libs, e.g. if the fix has a bug.

This is the complete test code:

package com.example.myapplication

import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.*

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Before
import org.junit.Rule

/**
 * Instrumented test, which will execute on an Android device.
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
@RunWith(AndroidJUnit4::class)
class CrashTest {


    val duration = 3000L
    val durationLongPress = 1000L

    @Composable
    fun SelectableText() {
        val text = """
            |Line
            |Line start selecting here and swipe over the empty lines
            |Line or select a word and extend it over the empty lines
            |Line
            |
            |
            |
            |Line
            |Line
            |Line
            |Line
            """.trimMargin()
        Column {
            SelectionContainer {
                Column {
                    Text("simple")
                    Text(text = text)   // works
                }
            }
            SelectionContainer {
                Column {
                    Text("crash")
                    text.lines().forEach {
                        Text(text = it)
                    }
                }
            }
            SelectionContainer {
                Column {
                    Text("space")
                    text.lines().forEach {
                        // empty lines replaced by a space works
                        Text(text = if (it == "") " " else it)
                    }
                }
            }
        }
    }

    @Rule
    @JvmField
    var test: ComposeContentTestRule = createComposeRule()

    @Before
    fun setUp() {
        test.setContent { SelectableText() }
        test.onRoot().printToLog("root")
    }

    val clock get() = test.mainClock

    fun inRealTime(what: String? = null, duration: Long = 0, todo: () -> Unit) {
        clock.autoAdvance = false
        what?.let { Log.d("%%%%%%%%%%", it) }
        val startVirt = clock.currentTime
        val startReal = System.currentTimeMillis()

        todo()

        while (true) {
            val virt = clock.currentTime - startVirt
            val real = System.currentTimeMillis() - startReal
            Log.d("..........", "virt: $virt real: $real")
            if (virt > real)
                Thread.sleep(1)
            else
                clock.advanceTimeByFrame()
            if ((virt > duration) and (real > duration))
                break
        }
        clock.autoAdvance = true
    }

    fun selectVertical(anchor: SemanticsNodeInteraction, parent: SemanticsNodeInteraction) {

        inRealTime("down(center)", durationLongPress) {
            anchor.performTouchInput {
                down(center)
            }
        }

        val nSteps = 100
        val timeStep = (duration-durationLongPress)/nSteps
        Log.d("----------", "timeStep = $timeStep")

        var step = Offset(1f,1f)
        parent.performTouchInput {
            step = (bottomCenter-topCenter)*0.8f/ nSteps.toFloat()
        }

        repeat(nSteps) {
            parent.performTouchInput {
                inRealTime("moveBy($step, $timeStep)", timeStep) {
                    moveBy(step)
                }
            }
        }

        parent.performTouchInput {
            inRealTime("up()") {
                up()
            }
        }
    }

    @Test
    fun works_simple() {
        val anchor = test.onNodeWithText("simple")
        val textNode = anchor.onParent()
        textNode.printToLog("simple")
        selectVertical(anchor, textNode)
    }

    @Test
    fun crash() {
        val anchor = test.onNodeWithText("crash")
        val textNode = anchor.onParent()
        textNode.printToLog("crash")
        selectVertical(anchor, textNode)
    }

    @Test
    fun works_space() {
        val anchor = test.onNodeWithText("space")
        val textNode = anchor.onParent()
        textNode.printToLog("space")
        selectVertical(anchor, textNode)
    }
}

Upvotes: 1

Related Questions