Reputation: 81
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
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