Skip to content

Commit

Permalink
Fix textfield cursor animation to not cause tests to hang.
Browse files Browse the repository at this point in the history
  • Loading branch information
m-sasha committed Feb 16, 2024
1 parent b1b9a0e commit eacc9a9
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import androidx.compose.foundation.AtomicReference
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.InfiniteAnimationPolicy
import kotlin.coroutines.coroutineContext
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.coroutineScope
Expand Down Expand Up @@ -52,7 +54,7 @@ internal class CursorAnimationState {
* Won't return until the animation cancelled via [cancelAndHide] or this coroutine's [Job] is
* cancelled. In both cases, the cursor will always end up hidden.
*/
suspend fun snapToVisibleAndAnimate() {
suspend fun snapToVisibleAndAnimate() = runCursorAnimation {
coroutineScope {
// Can't do a single atomic update because we need to get the old value before launching
// the new coroutine. So we set to null first, and then launch only if still null (i.e.
Expand Down Expand Up @@ -94,3 +96,20 @@ internal class CursorAnimationState {
job?.cancel()
}
}


/**
* Runs the infinite animation in [block], taking into account the current
* [InfiniteAnimationPolicy].
*
* This is needed to allow the text field cursor blinking to be cancelled by
* [InfiniteAnimationPolicy] as if it was an animation. Otherwise `waitForIdle` in tests with a
* focused text field will never return. Note that on Android this isn't needed because there
* `waitForIdle` appears to completely ignore delayed tasks (see
* https://issuetracker.google.com/issues/324768454 for details).
*/
suspend fun runCursorAnimation(block: suspend () -> Unit) =
when (val policy = coroutineContext[InfiniteAnimationPolicy]) {
null -> block()
else -> policy.onInfiniteOperation { block() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Brush
Expand All @@ -39,9 +42,12 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SkikoComposeUiTest
import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextReplacement
import androidx.compose.ui.test.runSkikoComposeUiTest
Expand Down Expand Up @@ -383,6 +389,37 @@ class TextFieldCursorTest {
)
}

@Test
fun cursorBlinkingDoesNotHangTestWithAutoAdvance() = runSkikoComposeUiTest {
mainClock.autoAdvance = true
cursorBlinkingDoesNotHangTest()
}

@Test
fun cursorBlinkingDoesNotHangTestWithoutAutoAdvance() = runSkikoComposeUiTest {
mainClock.autoAdvance = false
cursorBlinkingDoesNotHangTest()
}

private fun SkikoComposeUiTest.cursorBlinkingDoesNotHangTest() {
setContent {
val focusRequester = remember { FocusRequester() }
BasicTextField(
value = "",
onValueChange = { },
modifier = Modifier
.focusRequester(focusRequester)
.testTag("textfield")
)

LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}

onNodeWithTag("textfield").assertIsFocused()
}

private fun SkikoComposeUiTest.focusAndWait() {
onNode(hasSetTextAction()).performClick()
mainClock.advanceTimeUntil { isFocused }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,11 @@ class SkikoComposeUiTest(
scene = runOnUiThread(::createUi)
return block()
} finally {
// Close the scene before calling testScope.runTest so that all the coroutines are
// cancelled when we call it.
runOnUiThread(scene::close)
// call runTest instead of deprecated cleanupTestCoroutines()
testScope.runTest { }
runOnUiThread(scene::close)
uncaughtExceptionHandler.throwUncaught()
}
}
Expand Down

0 comments on commit eacc9a9

Please sign in to comment.