blob: 85c4f9ac095239b84e8cb74c5472c91abe4eb084 [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.foundation.text2
import android.os.Build
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.DefaultCursorThickness
import androidx.compose.foundation.text.FocusedWindowTest
import androidx.compose.foundation.text.TEST_FONT_FAMILY
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.foundation.text2.input.TextFieldState
import androidx.compose.foundation.text2.input.setTextAndPlaceCursorAtEnd
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.testutils.assertContainsColor
import androidx.compose.testutils.assertDoesNotContainColor
import androidx.compose.testutils.assertPixelColor
import androidx.compose.testutils.assertPixels
import androidx.compose.testutils.assertShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.MotionDurationScale
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
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.layout.layout
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.platform.WindowInfo
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performTextInputSelection
import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.toOffset
import androidx.test.filters.FlakyTest
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import kotlin.math.ceil
import kotlin.math.floor
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
@LargeTest
class TextFieldCursorTest : FocusedWindowTest {
private val motionDurationScale = object : MotionDurationScale {
override var scaleFactor: Float by mutableStateOf(1f)
}
@OptIn(ExperimentalTestApi::class)
@get:Rule
val rule = createComposeRule(effectContext = motionDurationScale).also {
it.mainClock.autoAdvance = false
}
private lateinit var state: TextFieldState
private val boxPadding = 8.dp
// Both TextField background and font color should be the same to make sure that only
// cursor is visible
private val contentColor = Color.White
private val cursorColor = Color.Red
private val fontSize = 10.sp
private val textStyle = TextStyle(
color = contentColor,
background = contentColor,
fontSize = fontSize,
fontFamily = TEST_FONT_FAMILY
)
private var isFocused = false
private var textLayoutResult: (() -> TextLayoutResult?)? = null
private val cursorRect: Rect
// assume selection is collapsed
get() = textLayoutResult?.invoke()?.getCursorRect(state.text.selectionInChars.start)
?: Rect.Zero
private val cursorSize: DpSize by lazy {
with(rule.density) {
DpSize(DefaultCursorThickness, fontSize.toDp())
}
}
private val cursorSizePx: Size by lazy {
with(rule.density) { cursorSize.toSize() }
}
private val cursorTopCenterInLtr: Offset
// assume selection is collapsed
get() = cursorRect.topCenter + Offset(cursorSizePx.width / 2f, 0f)
private val cursorTopCenterInRtl: Offset
// assume selection is collapsed
get() = cursorRect.topCenter - Offset(cursorSizePx.width / 2f, 0f)
private val backgroundModifier = Modifier.background(contentColor)
private val focusModifier = Modifier.onFocusChanged { if (it.isFocused) isFocused = true }
// default TextFieldModifier
private val textFieldModifier = Modifier
.then(backgroundModifier)
.then(focusModifier)
// default onTextLayout to capture cursor boundaries.
private val onTextLayout: Density.(() -> TextLayoutResult?) -> Unit = { textLayoutResult = it }
private fun ComposeContentTestRule.setTestContent(
content: @Composable () -> Unit
) {
this.setTextFieldTestContent {
// The padding helps if the test is run accidentally in landscape. Landscape makes
// the cursor to be next to the navigation bar which affects the red color to be a
// bit different - possibly anti-aliasing.
Box(Modifier.padding(boxPadding)) {
content()
}
}
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun textFieldFocused_cursorRendered() {
state = TextFieldState()
rule.setTestContent {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier,
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
focusAndWait()
rule.mainClock.advanceTimeBy(100)
rule.onNode(hasSetTextAction())
.captureToImage()
.assertCursor(cursorTopCenterInLtr)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun textFieldFocused_cursorRendered_rtlLayout() {
state = TextFieldState()
rule.setTestContent {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier.width(30.dp),
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
}
focusAndWait()
rule.mainClock.advanceTimeBy(100)
// an empty text layout will be placed on the right side of 30.dp-width area
// cursor will be at the most right side
rule.onNode(hasSetTextAction())
.captureToImage()
.assertCursor(cursorTopCenterInRtl)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun textFieldFocused_cursorRendered_rtlText_ltrLayout() {
state = TextFieldState("\u05D0\u05D1\u05D2", TextRange(3))
rule.setTestContent {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier,
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
focusAndWait()
rule.mainClock.advanceTimeBy(100)
rule.onNode(hasSetTextAction())
.captureToImage()
.assertCursor(cursorTopCenterInLtr)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun textFieldFocused_cursorRendered_rtlTextLayout() {
state = TextFieldState("\u05D0\u05D1\u05D2", TextRange(3))
rule.setTestContent {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier.width(50.dp),
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
}
focusAndWait()
rule.mainClock.advanceTimeBy(100)
rule.onNode(hasSetTextAction())
.captureToImage()
// 20 - 2(cursor)
.assertCursor(cursorTopCenterInRtl)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun textFieldCursorAtTheEnd_coercedIntoView() {
state = TextFieldState("hello", TextRange(5))
rule.setTestContent {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier.width(50.dp),
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
focusAndWait()
rule.mainClock.advanceTimeBy(100)
rule.onNode(hasSetTextAction())
.captureToImage()
.assertCursor(cursorTopCenterInLtr - Offset(cursorSizePx.width, 0f))
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun textFieldCursorAtTheEnd_coercedIntoView_rtl() {
state = TextFieldState("\u05D0\u05D1\u05D2", TextRange(3))
rule.setTestContent {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier.width(30.dp),
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
}
focusAndWait()
rule.mainClock.advanceTimeBy(100)
rule.onNode(hasSetTextAction())
.captureToImage()
.assertCursor(cursorTopCenterInRtl + Offset(cursorSizePx.width, 0f))
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun textFieldFocused_cursorWithBrush() {
state = TextFieldState()
rule.setTestContent {
BasicTextField2(
state = state,
textStyle = textStyle.copy(fontSize = textStyle.fontSize * 2),
modifier = Modifier
.then(backgroundModifier)
.then(focusModifier),
cursorBrush = Brush.verticalGradient(
// make a brush double/triple color at the beginning and end so we have
// stable colors at the ends.
// Without triple bottom, the bottom color never hits to the provided color.
listOf(
Color.Blue,
Color.Blue,
Color.Green,
Color.Green,
Color.Green
)
),
onTextLayout = onTextLayout
)
}
focusAndWait()
rule.mainClock.advanceTimeBy(100)
val bitmap = rule.onNode(hasSetTextAction())
.captureToImage().toPixelMap()
val cursorLeft = ceil(cursorRect.left).toInt() + 1
val cursorTop = ceil(cursorRect.top).toInt() + 1
val cursorBottom = floor(cursorRect.bottom).toInt() - 1
bitmap.assertPixelColor(Color.Blue, x = cursorLeft, y = cursorTop)
bitmap.assertPixelColor(Color.Green, x = cursorLeft, y = cursorBottom)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun cursorBlinkingAnimation() {
state = TextFieldState()
rule.setTestContent {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier,
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
focusAndWait()
// cursor visible first 500 ms
rule.mainClock.advanceTimeBy(100)
rule.onNode(hasSetTextAction())
.captureToImage()
.assertCursor(cursorTopCenterInLtr)
// cursor invisible during next 500 ms
rule.mainClock.advanceTimeBy(700)
rule.onNode(hasSetTextAction())
.captureToImage()
.assertShape(
density = rule.density,
shape = RectangleShape,
shapeColor = contentColor,
backgroundColor = contentColor,
shapeOverlapPixelCount = 0.0f
)
}
@Suppress("UnnecessaryOptInAnnotation")
@OptIn(ExperimentalTestApi::class)
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun cursorBlinkingAnimation_whenSystemDisablesAnimations() {
motionDurationScale.scaleFactor = 0f
state = TextFieldState()
rule.setTestContent {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier,
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
focusAndWait()
// cursor visible first 500 ms
rule.mainClock.advanceTimeBy(100)
rule.onNode(hasSetTextAction())
.captureToImage()
.assertCursor(cursorTopCenterInLtr)
// cursor invisible during next 500 ms
rule.mainClock.advanceTimeBy(700)
rule.onNode(hasSetTextAction())
.captureToImage()
.assertShape(
density = rule.density,
shape = RectangleShape,
shapeColor = contentColor,
backgroundColor = contentColor,
shapeOverlapPixelCount = 0.0f
)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun cursorUnsetColor_noCursor() {
state = TextFieldState("hello", initialSelectionInChars = TextRange(2))
rule.setTestContent {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier,
cursorBrush = SolidColor(Color.Unspecified)
)
}
focusAndWait()
// no cursor when usually shown
rule.onNode(hasSetTextAction())
.captureToImage()
.assertShape(
density = rule.density,
shape = RectangleShape,
shapeColor = contentColor,
backgroundColor = contentColor,
shapeOverlapPixelCount = 0.0f
)
// no cursor when should be no cursor
rule.mainClock.advanceTimeBy(700)
rule.onNode(hasSetTextAction())
.captureToImage()
.assertShape(
density = rule.density,
shape = RectangleShape,
shapeColor = contentColor,
backgroundColor = contentColor,
shapeOverlapPixelCount = 0.0f
)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@FlakyTest(bugId = 303503435)
fun cursorNotBlinking_whileTyping() {
state = TextFieldState("test", initialSelectionInChars = TextRange(4))
rule.setTestContent {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier.width(100.dp),
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
focusAndWait()
// cursor visible first 500 ms
rule.mainClock.advanceTimeBy(500)
// TODO(b/170298051) check here that cursor is visible when we have a way to control
// cursor position when sending a text
// change text field value
rule.onNode(hasSetTextAction())
.performTextInput("s")
// cursor would have been invisible during next 500 ms if cursor blinks while typing.
// To prevent blinking while typing we restart animation when new symbol is typed.
rule.mainClock.advanceTimeBy(300)
rule.onNode(hasSetTextAction())
.captureToImage()
.assertCursor(cursorTopCenterInLtr)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@FlakyTest(bugId = 303903824)
fun selectionChanges_cursorNotBlinking() {
state = TextFieldState("test", initialSelectionInChars = TextRange(2))
rule.setTestContent {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier,
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
focusAndWait()
// hide the cursor
rule.mainClock.advanceTimeBy(500)
rule.mainClock.advanceTimeByFrame()
// TODO(b/170298051) check here that cursor is visible when we have a way to control
// cursor position when sending a text
rule.onNode(hasSetTextAction())
.performTextInputSelection(TextRange(0))
// necessary for animation to start (shows cursor again)
rule.mainClock.advanceTimeByFrame()
rule.onNode(hasSetTextAction())
.captureToImage()
.assertCursor(cursorTopCenterInLtr)
}
@Ignore("b/285407485")
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun brushChanged_doesntResetTimer() {
var cursorBrush by mutableStateOf(SolidColor(cursorColor))
state = TextFieldState()
rule.setTestContent {
Box(Modifier.padding(boxPadding)) {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier,
cursorBrush = cursorBrush,
onTextLayout = onTextLayout
)
}
}
focusAndWait()
rule.mainClock.advanceTimeBy(800)
cursorBrush = SolidColor(Color.Green)
rule.mainClock.advanceTimeByFrame()
rule.onNode(hasSetTextAction())
.captureToImage()
.assertShape(
density = rule.density,
shape = RectangleShape,
shapeColor = contentColor,
backgroundColor = contentColor,
shapeOverlapPixelCount = 0.0f
)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun selectionNotCollapsed_cursorNotDrawn() {
state = TextFieldState("test", initialSelectionInChars = TextRange(2, 3))
rule.setTestContent {
// set selection highlight to a known color
CompositionLocalProvider(
LocalTextSelectionColors provides TextSelectionColors(Color.Blue, Color.Blue)
) {
BasicTextField2(
state = state,
// make sure that background is not obstructing selection
textStyle = textStyle.copy(
background = Color.Unspecified
),
modifier = textFieldModifier,
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
}
focusAndWait()
// cursor should still be visible if there wasn't a selection
rule.mainClock.advanceTimeBy(300)
rule.mainClock.advanceTimeByFrame()
rule.onNode(hasSetTextAction())
.captureToImage()
.assertDoesNotContainColor(cursorColor)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun focusLost_cursorHidesImmediately() {
state = TextFieldState("test")
rule.setTestContent {
Column {
BasicTextField2(
state = state,
// make sure that background is not obstructing selection
textStyle = textStyle,
modifier = textFieldModifier,
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
Box(
modifier = Modifier
.focusable(true)
.testTag("box")
)
}
}
focusAndWait()
rule.mainClock.advanceTimeBy(100)
rule.mainClock.advanceTimeByFrame()
rule.onNode(hasSetTextAction())
.captureToImage()
.assertCursor(cursorTopCenterInLtr)
rule.onNodeWithTag("box").requestFocus()
rule.mainClock.advanceTimeByFrame()
// cursor should hide immediately.
rule.onNode(hasSetTextAction())
.captureToImage()
.assertShape(
density = rule.density,
shape = RectangleShape,
shapeColor = contentColor,
backgroundColor = contentColor,
shapeOverlapPixelCount = 0.0f
)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun readOnly_cursorIsNotDrawn() {
state = TextFieldState("test", initialSelectionInChars = TextRange(4))
rule.setTestContent {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier,
readOnly = true,
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
focusAndWait()
rule.mainClock.advanceTimeBy(100)
rule.mainClock.advanceTimeByFrame()
rule.onNode(hasSetTextAction())
.captureToImage()
.assertDoesNotContainColor(cursorColor)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun toggling_readOnly_drawsCursorAgain() {
var readOnly by mutableStateOf(true)
state = TextFieldState("test", initialSelectionInChars = TextRange(4))
rule.setTestContent {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier,
readOnly = readOnly,
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
focusAndWait()
rule.mainClock.advanceTimeBy(100)
rule.mainClock.advanceTimeByFrame()
rule.onNode(hasSetTextAction())
.captureToImage()
.assertDoesNotContainColor(cursorColor)
readOnly = false
rule.mainClock.advanceTimeByFrame()
rule.onNode(hasSetTextAction())
.captureToImage()
.assertCursor(cursorTopCenterInLtr)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun cursorNotBlinking_whenWindowLostFocus() {
state = TextFieldState()
val focusWindow = mutableStateOf(true)
fun createWindowInfo(focused: Boolean) = object : WindowInfo {
override val isWindowFocused: Boolean
get() = focused
}
rule.setTestContent {
CompositionLocalProvider(LocalWindowInfo provides createWindowInfo(focusWindow.value)) {
Box(Modifier.padding(boxPadding)) {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier,
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
}
}
focusAndWait()
// cursor visible first 500ms
rule.mainClock.advanceTimeBy(100)
rule.onNode(hasSetTextAction())
.captureToImage()
.assertContainsColor(cursorColor)
// window loses focus
focusWindow.value = false
rule.waitForIdle()
// check that text field cursor disappeared even within visible 500ms
rule.mainClock.advanceTimeBy(300)
rule.onNode(hasSetTextAction())
.captureToImage()
.assertDoesNotContainColor(cursorColor)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun focusedTextField_resumeBlinking_whenWindowRegainsFocus() {
state = TextFieldState()
val focusWindow = mutableStateOf(true)
fun createWindowInfo(focused: Boolean) = object : WindowInfo {
override val isWindowFocused: Boolean
get() = focused
}
rule.setTestContent {
CompositionLocalProvider(LocalWindowInfo provides createWindowInfo(focusWindow.value)) {
Box(Modifier.padding(boxPadding)) {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier,
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
}
}
focusAndWait()
// window loses focus
focusWindow.value = false
rule.waitForIdle()
// check that text field cursor disappeared even within visible 500ms
rule.mainClock.advanceTimeBy(100)
rule.onNode(hasSetTextAction())
.captureToImage()
.assertDoesNotContainColor(cursorColor)
// window regains focus within 500ms
focusWindow.value = true
rule.waitForIdle()
rule.mainClock.advanceTimeBy(100)
rule.onNode(hasSetTextAction())
.captureToImage()
.assertContainsColor(cursorColor)
.assertCursor(cursorTopCenterInLtr)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun textField_keepsSelection_whenWindowLosesFocus() {
state = TextFieldState("hello", initialSelectionInChars = TextRange(0, 5))
val selectionColor = Color.Blue
val focusWindow = mutableStateOf(true)
val windowInfo = object : WindowInfo {
override val isWindowFocused: Boolean
get() = focusWindow.value
}
rule.setTestContent {
CompositionLocalProvider(
LocalWindowInfo provides windowInfo,
LocalTextSelectionColors provides TextSelectionColors(
selectionColor,
selectionColor
)
) {
BasicTextField2(
state = state,
// make sure that background is not obstructing selection
textStyle = textStyle.copy(background = Color.Unspecified),
modifier = textFieldModifier,
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
}
rule.onNode(hasSetTextAction())
.captureToImage()
.assertContainsColor(selectionColor)
// window lost focus, make sure selection still drawn
focusWindow.value = false
rule.waitForIdle()
rule.onNode(hasSetTextAction())
.captureToImage()
.assertContainsColor(selectionColor)
}
private fun focusAndWait() {
rule.onNode(hasSetTextAction()).requestFocus()
rule.mainClock.advanceTimeUntil { isFocused }
}
/**
* @param cursorPosition Top center of cursor rectangle
*/
private fun ImageBitmap.assertCursor(cursorPosition: Offset) {
assertThat(cursorPosition.x).isAtLeast(0f)
assertThat(cursorPosition.y).isAtLeast(0f)
// assert cursor width is greater than 2 since we will shrink the check area by 1 on each
// side
assertThat(cursorSizePx.width).isGreaterThan(2)
// shrink the check are by 1px for left, top, right, bottom
val checkRect = Rect(
ceil(cursorPosition.x - cursorSizePx.width / 2) + 1,
ceil(cursorPosition.y) + 1,
floor(cursorPosition.x + cursorSizePx.width / 2) - 1,
floor(cursorPosition.y + cursorSizePx.height) - 1
)
// skip an expanded rectangle that is 1px larger than cursor rectangle due to antialiasing
val skipRect = Rect(
floor(cursorPosition.x - cursorSizePx.width / 2) - 1,
floor(cursorPosition.y) - 1,
ceil(cursorPosition.x + cursorSizePx.width / 2) + 1,
ceil(cursorPosition.y + cursorSizePx.height) + 1
)
val width = width
val height = height
this.assertPixels(
IntSize(width, height)
) { position ->
if (checkRect.contains(position.toOffset())) {
// cursor
cursorColor
} else if (skipRect.contains(position.toOffset())) {
// skip some pixels around cursor
null
} else {
// text field background
contentColor
}
}
}
@Test
fun textFieldCursor_alwaysReadLatestState_duringDraw() {
state = TextFieldState("hello world", TextRange(5))
rule.setTestContent {
Box(Modifier.padding(boxPadding)) {
BasicTextField2(
state = state,
textStyle = textStyle,
modifier = textFieldModifier.layout { measurable, constraints ->
// change the state during layout so draw can read the new state
val currValue = state.text
if (currValue.isNotEmpty()) {
val newText = currValue.dropLast(1)
state.setTextAndPlaceCursorAtEnd(newText.toString())
}
val p = measurable.measure(constraints)
layout(p.width, p.height) {
p.place(0, 0)
}
},
cursorBrush = SolidColor(cursorColor),
onTextLayout = onTextLayout
)
}
}
rule.waitForIdle()
rule.onNode(hasSetTextAction()).assertTextEquals("")
// this test just needs to finish without crashing. There is no other assertion
}
}