blob: fa4a4be1a812fb9d4c903f1b5eab1f9379a1a9f2 [file] [log] [blame]
/*
* Copyright 2022 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.material3
import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.testutils.assertPixels
import androidx.compose.testutils.assertShape
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.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.node.Ref
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalTextInputService
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.error
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.click
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.PlatformTextInputService
import androidx.compose.ui.text.input.TextInputService
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.atLeastOnce
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import kotlin.math.roundToInt
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@OptIn(ExperimentalMaterial3Api::class)
@MediumTest
@RunWith(AndroidJUnit4::class)
class OutlinedTextFieldTest {
private val ExpectedMinimumTextFieldHeight = TextFieldDefaults.MinHeight
private val ExpectedDefaultTextFieldWidth = TextFieldDefaults.MinWidth
private val ExpectedPadding = TextFieldPadding
private val IconPadding = HorizontalIconPadding
private val TextFieldTag = "textField"
@get:Rule
val rule = createComposeRule()
@Test
fun testOutlinedTextField_setSmallWidth() {
rule.setMaterialContentForSizeAssertions {
OutlinedTextField(
value = "input",
onValueChange = {},
modifier = Modifier.requiredWidth(40.dp)
)
}
.assertWidthIsEqualTo(40.dp)
}
@Test
fun testOutlinedTextField_defaultWidth() {
rule.setMaterialContentForSizeAssertions {
OutlinedTextField(
value = "input",
onValueChange = {}
)
}
.assertWidthIsEqualTo(ExpectedDefaultTextFieldWidth)
}
@Test
fun testOutlinedTextFields_singleFocus() {
var textField1Focused = false
val textField1Tag = "TextField1"
var textField2Focused = false
val textField2Tag = "TextField2"
rule.setMaterialContent(lightColorScheme()) {
Column {
OutlinedTextField(
modifier = Modifier
.testTag(textField1Tag)
.onFocusChanged { textField1Focused = it.isFocused },
value = "input1",
onValueChange = {}
)
OutlinedTextField(
modifier = Modifier
.testTag(textField2Tag)
.onFocusChanged { textField2Focused = it.isFocused },
value = "input2",
onValueChange = {}
)
}
}
rule.onNodeWithTag(textField1Tag).performClick()
rule.runOnIdle {
assertThat(textField1Focused).isTrue()
assertThat(textField2Focused).isFalse()
}
rule.onNodeWithTag(textField2Tag).performClick()
rule.runOnIdle {
assertThat(textField1Focused).isFalse()
assertThat(textField2Focused).isTrue()
}
}
@Test
fun testOutlinedTextField_getFocus_whenClickedOnInternalArea() {
var focused = false
rule.setMaterialContent(lightColorScheme()) {
Box {
OutlinedTextField(
modifier = Modifier
.testTag(TextFieldTag)
.onFocusChanged { focused = it.isFocused },
value = "input",
onValueChange = {}
)
}
}
// Click on (2, 2) which is a background area and outside input area
rule.onNodeWithTag(TextFieldTag).performTouchInput {
click(Offset(2f, 2f))
}
rule.runOnIdleWithDensity {
assertThat(focused).isTrue()
}
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun testOutlinedTextField_noTopPadding_ifNoLabel() {
val density = Density(4f)
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalDensity provides density) {
Box(Modifier.testTag("box").background(Color.Red)) {
OutlinedTextField(
value = "",
onValueChange = {},
colors = TextFieldDefaults.outlinedTextFieldColors(
unfocusedTextColor = Color.White,
unfocusedBorderColor = Color.White
),
shape = RectangleShape
)
}
}
}
rule.onNodeWithTag("box").captureToImage().assertShape(
density = density,
horizontalPadding = 1.dp, // OutlinedTextField border thickness
verticalPadding = 1.dp, // OutlinedTextField border thickness
backgroundColor = Color.White, // OutlinedTextField border color
shapeColor = Color.Red, // Color of background as OutlinedTextField is transparent
shape = RectangleShape
)
}
@Test
fun testOutlinedTextField_labelPosition_initial_singleLine() {
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
singleLine = true,
label = {
Box(Modifier
.size(MinTextLineHeight)
.onGloballyPositioned {
labelPosition.value = it.positionInRoot()
}
)
}
)
}
rule.runOnIdleWithDensity {
// x position is start + padding
assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
// y position is centered, plus additional padding allowance on top
assertThat(labelPosition.value?.y).isWithin(1f).of(
((ExpectedMinimumTextFieldHeight - MinTextLineHeight) / 2 +
OutlinedTextFieldTopPadding).toPx()
)
}
}
@Test
fun testOutlinedTextField_labelPosition_initial_withDefaultHeight() {
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
label = {
Box(Modifier
.size(MinTextLineHeight)
.onGloballyPositioned {
labelPosition.value = it.positionInRoot()
}
)
}
)
}
rule.runOnIdleWithDensity {
// x position is start + padding
assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
// y position is top + default padding + label padding allowance
assertThat(labelPosition.value?.y).isWithin(1f).of(
(ExpectedPadding + OutlinedTextFieldTopPadding).toPx()
)
}
}
@Test
fun testOutlinedTextField_labelPosition_initial_withMultiLineLabel() {
val textFieldWidth = 200.dp
val labelSize = Ref<IntSize>()
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier.requiredWidth(textFieldWidth),
label = {
Text(
text = "long long long long long long long long long long long long",
modifier = Modifier.onGloballyPositioned {
labelSize.value = it.size
labelPosition.value = it.positionInRoot()
}
)
}
)
}
rule.runOnIdleWithDensity {
// label size
assertThat(labelSize.value).isNotNull()
assertThat(labelSize.value?.height).isGreaterThan(0)
assertThat(labelSize.value?.width)
.isEqualTo(textFieldWidth.roundToPx() - 2 * ExpectedPadding.roundToPx())
// x position is start + padding
assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
// y position is top + default padding + label padding allowance
assertThat(labelPosition.value?.y).isWithin(1f).of(
(ExpectedPadding + OutlinedTextFieldTopPadding).toPx()
)
}
}
@Test
fun testOutlinedTextField_labelPosition_whenFocused() {
val labelPosition = Ref<Offset>()
val labelSize = MinFocusedLabelLineHeight
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
modifier = Modifier.testTag(TextFieldTag),
value = "",
onValueChange = {},
label = {
Box(Modifier
.size(MinFocusedLabelLineHeight)
.onGloballyPositioned {
labelPosition.value = it.positionInRoot()
}
)
}
)
}
// click to focus
rule.onNodeWithTag(TextFieldTag).performClick()
rule.runOnIdleWithDensity {
assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
assertThat(labelPosition.value?.y).isWithin(1f).of(
getLabelPosition(labelSize.roundToPx()).toFloat()
)
}
}
@Test
fun testOutlinedTextField_labelPosition_whenFocused_withMultiLineLabel() {
val textFieldWidth = 200.dp
val labelSize = Ref<IntSize>()
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier.testTag(TextFieldTag).requiredWidth(textFieldWidth),
label = {
Text(
text = "long long long long long long long long long long long long",
modifier = Modifier.onGloballyPositioned {
labelSize.value = it.size
labelPosition.value = it.positionInRoot()
}
)
}
)
}
// click to focus
rule.onNodeWithTag(TextFieldTag).performClick()
rule.runOnIdleWithDensity {
// label size
assertThat(labelSize.value).isNotNull()
assertThat(labelSize.value?.height).isGreaterThan(0)
assertThat(labelSize.value?.width!!.toFloat()).isWithin(1f).of(
(textFieldWidth - ExpectedPadding * 2).toPx()
)
// label position
assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
assertThat(labelPosition.value?.y).isWithin(1f).of(
getLabelPosition(labelSize.value!!.height).toFloat()
)
}
}
@Test
fun testOutlinedTextField_labelWidth_isNotAffectedByTrailingIcon_whenFocused() {
val textFieldWidth = 100.dp
val labelRequestedWidth = 65.dp
val labelSize = Ref<IntSize>()
val trailingSize = Ref<IntSize>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier.testTag(TextFieldTag).requiredWidth(textFieldWidth),
label = {
Text(
text = "Label",
modifier = Modifier.width(labelRequestedWidth).onGloballyPositioned {
labelSize.value = it.size
}
)
},
trailingIcon = {
Icon(
Icons.Default.Favorite,
null,
modifier = Modifier.onGloballyPositioned {
trailingSize.value = it.size
},
)
},
)
}
// click to focus
rule.onNodeWithTag(TextFieldTag).performClick()
rule.runOnIdleWithDensity {
assertThat(labelSize.value).isNotNull()
assertThat(trailingSize.value).isNotNull()
// First, check that label's requested size would be too wide if it's on the same line
// as the icon + padding
assertThat((labelRequestedWidth + IconPadding).roundToPx() + trailingSize.value!!.width)
.isGreaterThan(textFieldWidth.roundToPx())
// Next, assert that the requested size is satisfied anyway because the trailing icon
// does not affect it.
assertThat(labelSize.value?.width).isEqualTo(labelRequestedWidth.roundToPx())
}
}
@Test
fun testOutlinedTextField_labelPosition_whenInput() {
val labelSize = MinFocusedLabelLineHeight
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "input",
onValueChange = {},
label = {
Box(Modifier
.size(labelSize)
.onGloballyPositioned {
labelPosition.value = it.positionInRoot()
}
)
}
)
}
rule.runOnIdleWithDensity {
// label position
assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
assertThat(labelPosition.value?.y).isWithin(1f).of(
getLabelPosition(labelSize.roundToPx()).toFloat()
)
}
}
@Test
fun testOutlinedTextField_transparentComponents_doNotAppearInComposition() {
// Regression test for b/251162419
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text(text = "Label") },
placeholder = {
Text(text = "Placeholder", modifier = Modifier.testTag("Placeholder"))
},
prefix = {
Text(text = "Prefix", modifier = Modifier.testTag("Prefix"))
},
suffix = {
Text(text = "Suffix", modifier = Modifier.testTag("Suffix"))
}
)
}
rule.onNodeWithTag("Placeholder", useUnmergedTree = true).assertDoesNotExist()
rule.onNodeWithTag("Prefix", useUnmergedTree = true).assertDoesNotExist()
rule.onNodeWithTag("Suffix", useUnmergedTree = true).assertDoesNotExist()
}
@Test
fun testOutlinedTextField_placeholderPosition_withLabel() {
val placeholderSize = MinTextLineHeight
val placeholderPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
Box {
OutlinedTextField(
modifier = Modifier.testTag(TextFieldTag),
value = "",
onValueChange = {},
label = { Box(Modifier.size(MinFocusedLabelLineHeight)) },
placeholder = {
Box(Modifier
.size(placeholderSize)
.onGloballyPositioned {
placeholderPosition.value = it.positionInRoot()
}
)
}
)
}
}
// click to focus
rule.onNodeWithTag(TextFieldTag).performClick()
rule.runOnIdleWithDensity {
assertThat(placeholderPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
assertThat(placeholderPosition.value?.y).isWithin(1f).of(
(ExpectedPadding + OutlinedTextFieldTopPadding).toPx()
)
}
}
@Test
fun testOutlinedTextField_placeholderPosition_whenNoLabel() {
val placeholderSize = MinTextLineHeight
val placeholderPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
modifier = Modifier.testTag(TextFieldTag),
value = "",
onValueChange = {},
placeholder = {
Box(Modifier
.size(placeholderSize)
.onGloballyPositioned {
placeholderPosition.value = it.positionInRoot()
}
)
}
)
}
// click to focus
rule.onNodeWithTag(TextFieldTag).performClick()
rule.runOnIdleWithDensity {
assertThat(placeholderPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
assertThat(placeholderPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
}
}
@Test
fun testOutlinedTextField_noPlaceholder_whenInputNotEmpty() {
val placeholderSize = Ref<IntSize>()
val placeholderPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
modifier = Modifier.testTag(TextFieldTag),
value = "input",
onValueChange = {},
placeholder = {
Text(
text = "placeholder",
modifier = Modifier.onGloballyPositioned {
placeholderPosition.value = it.positionInRoot()
placeholderSize.value = it.size
}
)
}
)
}
// click to focus
rule.onNodeWithTag(TextFieldTag).performClick()
rule.runOnIdleWithDensity {
assertThat(placeholderSize.value).isNull()
assertThat(placeholderPosition.value).isNull()
}
}
@Test
fun testOutlinedTextField_placeholderColorAndTextStyle() {
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
modifier = Modifier.testTag(TextFieldTag),
value = "",
onValueChange = {},
placeholder = {
Text("placeholder")
assertThat(LocalTextStyle.current)
.isEqualTo(MaterialTheme.typography.bodyLarge)
}
)
}
// click to focus
rule.onNodeWithTag(TextFieldTag).performClick()
}
@Test
fun testOutlinedTextField_labelAndPlaceholderPosition_whenSmallerThanMinimumHeight() {
val labelSize = 10.dp
val labelPosition = Ref<Offset>()
val placeholderSize = 20.dp
val placeholderPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
modifier = Modifier.testTag(TextFieldTag),
value = "",
onValueChange = {},
label = {
Box(Modifier
.size(labelSize)
.onGloballyPositioned {
labelPosition.value = it.positionInRoot()
}
)
},
placeholder = {
Box(Modifier
.size(placeholderSize)
.onGloballyPositioned {
placeholderPosition.value = it.positionInRoot()
}
)
}
)
}
// click to focus
rule.onNodeWithTag(TextFieldTag).performClick()
rule.runOnIdleWithDensity {
// size
assertThat(labelSize).isLessThan(MinFocusedLabelLineHeight)
assertThat(placeholderSize).isLessThan(MinTextLineHeight)
// label position
assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
assertThat(labelPosition.value?.y).isWithin(1f).of(
getLabelPosition(labelSize.roundToPx()).toFloat()
)
// placeholder position
assertThat(placeholderPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
// placeholder y position is top + default padding + label padding allowance, then
// centered within allocated space
assertThat(placeholderPosition.value?.y).isWithin(1f).of(
(ExpectedPadding + OutlinedTextFieldTopPadding +
(MinTextLineHeight - placeholderSize) / 2).toPx()
)
}
}
@Test
fun testOutlinedTextField_trailingAndLeading_sizeAndPosition_defaultIcon() {
val textFieldWidth = 300.dp
val leadingPosition = Ref<Offset>()
val leadingSize = Ref<IntSize>()
val trailingPosition = Ref<Offset>()
val trailingSize = Ref<IntSize>()
val density = Density(2f)
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalDensity provides density) {
OutlinedTextField(
value = "text",
onValueChange = {},
modifier = Modifier.width(textFieldWidth),
label = { Text("label") },
leadingIcon = {
Icon(
Icons.Default.Favorite,
null,
Modifier.onGloballyPositioned {
leadingPosition.value = it.positionInRoot()
leadingSize.value = it.size
}
)
},
trailingIcon = {
Icon(
Icons.Default.Favorite,
null,
Modifier.onGloballyPositioned {
trailingPosition.value = it.positionInRoot()
trailingSize.value = it.size
}
)
},
)
}
}
rule.runOnIdle {
with(density) {
val minimumHeight = ExpectedMinimumTextFieldHeight.roundToPx()
val size = 24.dp // default icon size
// leading
assertThat(leadingSize.value).isEqualTo(IntSize(size.roundToPx(), size.roundToPx()))
assertThat(leadingPosition.value?.x).isEqualTo(IconPadding.roundToPx().toFloat())
assertThat(leadingPosition.value?.y).isEqualTo(
((minimumHeight - leadingSize.value!!.height) / 2f).roundToInt() +
8.dp.roundToPx()
)
// trailing
assertThat(trailingSize.value).isEqualTo(
IntSize(
size.roundToPx(),
size.roundToPx()
)
)
assertThat(trailingPosition.value?.x).isEqualTo(
(
textFieldWidth.roundToPx() - IconPadding.roundToPx() -
trailingSize.value!!.width
).toFloat()
)
assertThat(trailingPosition.value?.y).isEqualTo(
((minimumHeight - trailingSize.value!!.height) / 2f).roundToInt() +
8.dp.roundToPx()
)
}
}
}
@Test
fun testOutlinedTextField_trailingAndLeading_sizeAndPosition_defaultIconButton() {
val textFieldWidth = 300.dp
val textFieldHeight = 80.dp
val density = Density(2f)
var leadingPosition: Offset? = null
var leadingSize: IntSize? = null
var trailingPosition: Offset? = null
var trailingSize: IntSize? = null
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalDensity provides density) {
OutlinedTextField(
value = "text",
onValueChange = {},
modifier = Modifier.width(textFieldWidth).height(textFieldHeight),
leadingIcon = {
IconButton(
onClick = {},
modifier = Modifier.onGloballyPositioned {
leadingPosition = it.positionInRoot()
leadingSize = it.size
}
) { Icon(Icons.Default.Favorite, null) }
},
trailingIcon = {
IconButton(
onClick = {},
modifier = Modifier.onGloballyPositioned {
trailingPosition = it.positionInRoot()
trailingSize = it.size
}
) { Icon(Icons.Default.Favorite, null) }
}
)
}
}
rule.runOnIdle {
val size = 48.dp // default IconButton size
with(density) {
// leading
assertThat(leadingSize).isEqualTo(IntSize(size.roundToPx(), size.roundToPx()))
assertThat(leadingPosition?.x).isEqualTo(0f)
assertThat(leadingPosition?.y).isEqualTo(
((textFieldHeight.roundToPx() - leadingSize!!.height) / 2f).roundToInt()
)
// trailing
assertThat(trailingSize).isEqualTo(IntSize(size.roundToPx(), size.roundToPx()))
assertThat(trailingPosition?.x).isEqualTo(
(textFieldWidth.roundToPx() - trailingSize!!.width).toFloat()
)
assertThat(trailingPosition?.y).isEqualTo(
((textFieldHeight.roundToPx() - trailingSize!!.height) / 2f).roundToInt()
)
}
}
}
@Test
fun testOutlinedTextField_trailingAndLeading_sizeAndPosition_nonDefaultSizeIcon() {
val textFieldWidth = 300.dp
val textFieldHeight = 80.dp
val size = 72.dp
val density = Density(2f)
var leadingPosition: Offset? = null
var leadingSize: IntSize? = null
var trailingPosition: Offset? = null
var trailingSize: IntSize? = null
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalDensity provides density) {
OutlinedTextField(
value = "text",
onValueChange = {},
modifier = Modifier.width(textFieldWidth).height(textFieldHeight),
leadingIcon = {
Box(
Modifier.size(size).onGloballyPositioned {
leadingPosition = it.positionInRoot()
leadingSize = it.size
}
)
},
trailingIcon = {
Box(
Modifier.size(size).onGloballyPositioned {
trailingPosition = it.positionInRoot()
trailingSize = it.size
}
)
},
)
}
}
rule.runOnIdle {
with(density) {
// leading
assertThat(leadingSize).isEqualTo(IntSize(size.roundToPx(), size.roundToPx()))
assertThat(leadingPosition?.x).isEqualTo(0f)
assertThat(leadingPosition?.y).isEqualTo(
((textFieldHeight.roundToPx() - leadingSize!!.height) / 2f).roundToInt()
)
// trailing
assertThat(trailingSize).isEqualTo(
IntSize(
size.roundToPx(),
size.roundToPx()
)
)
assertThat(trailingPosition?.x).isEqualTo(
(textFieldWidth.roundToPx() - trailingSize!!.width).toFloat()
)
assertThat(trailingPosition?.y).isEqualTo(
((textFieldHeight.roundToPx() - trailingSize!!.height) / 2f).roundToInt()
)
}
}
}
@Test
fun testOutlinedTextField_prefixAndSuffixPosition_withLabel() {
val textFieldWidth = 300.dp
val prefixPosition = Ref<Offset>()
val prefixSize = MinTextLineHeight
val suffixPosition = Ref<Offset>()
val suffixSize = MinTextLineHeight
val density = Density(2f)
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalDensity provides density) {
OutlinedTextField(
value = "text",
onValueChange = {},
modifier = Modifier.width(textFieldWidth),
label = { Box(Modifier.size(MinFocusedLabelLineHeight)) },
prefix = {
Box(Modifier
.size(prefixSize)
.onGloballyPositioned {
prefixPosition.value = it.positionInRoot()
}
)
},
suffix = {
Box(Modifier
.size(suffixSize)
.onGloballyPositioned {
suffixPosition.value = it.positionInRoot()
}
)
}
)
}
}
rule.runOnIdle {
with(density) {
// prefix
assertThat(prefixPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
assertThat(prefixPosition.value?.y).isWithin(1f).of(
(ExpectedPadding + OutlinedTextFieldTopPadding).toPx()
)
// suffix
assertThat(suffixPosition.value?.x).isWithin(1f).of(
(textFieldWidth - ExpectedPadding - suffixSize).toPx()
)
assertThat(suffixPosition.value?.y).isWithin(1f).of(
(ExpectedPadding + OutlinedTextFieldTopPadding).toPx()
)
}
}
}
@Test
fun testOutlinedTextField_prefixAndSuffixPosition_whenNoLabel() {
val textFieldWidth = 300.dp
val prefixPosition = Ref<Offset>()
val prefixSize = MinTextLineHeight
val suffixPosition = Ref<Offset>()
val suffixSize = MinTextLineHeight
val density = Density(2f)
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalDensity provides density) {
OutlinedTextField(
value = "text",
onValueChange = {},
modifier = Modifier.width(textFieldWidth),
prefix = {
Box(Modifier
.size(prefixSize)
.onGloballyPositioned {
prefixPosition.value = it.positionInRoot()
}
)
},
suffix = {
Box(Modifier
.size(suffixSize)
.onGloballyPositioned {
suffixPosition.value = it.positionInRoot()
}
)
}
)
}
}
rule.runOnIdle {
with(density) {
// prefix
assertThat(prefixPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
assertThat(prefixPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
// suffix
assertThat(suffixPosition.value?.x).isWithin(1f).of(
(textFieldWidth - ExpectedPadding - suffixSize).toPx()
)
assertThat(suffixPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
}
}
}
@Test
fun testOutlinedTextField_prefixAndSuffixPosition_withIcons() {
val textFieldWidth = 300.dp
val prefixPosition = Ref<Offset>()
val prefixSize = MinTextLineHeight
val suffixPosition = Ref<Offset>()
val suffixSize = MinTextLineHeight
val density = Density(2f)
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalDensity provides density) {
OutlinedTextField(
value = "text",
onValueChange = {},
modifier = Modifier.width(textFieldWidth),
prefix = {
Box(Modifier
.size(prefixSize)
.onGloballyPositioned {
prefixPosition.value = it.positionInRoot()
}
)
},
suffix = {
Box(Modifier
.size(suffixSize)
.onGloballyPositioned {
suffixPosition.value = it.positionInRoot()
}
)
},
leadingIcon = { Icon(Icons.Default.Favorite, null) },
trailingIcon = { Icon(Icons.Default.Favorite, null) },
)
}
}
rule.runOnIdle {
with(density) {
val iconSize = 24.dp // default icon size
// prefix
assertThat(prefixPosition.value?.x).isWithin(1f).of(
(ExpectedPadding + IconPadding + iconSize).toPx()
)
assertThat(prefixPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
// suffix
assertThat(suffixPosition.value?.x).isWithin(1f).of(
(textFieldWidth - IconPadding - iconSize - ExpectedPadding - suffixSize).toPx()
)
assertThat(suffixPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
}
}
}
@Test
fun testOutlinedTextField_labelPositionX_initial_withTrailingAndLeading() {
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
label = {
Text(
text = "label",
modifier = Modifier.onGloballyPositioned {
labelPosition.value = it.positionInRoot()
}
)
},
trailingIcon = { Icon(Icons.Default.Favorite, null) },
leadingIcon = { Icon(Icons.Default.Favorite, null) }
)
}
rule.runOnIdleWithDensity {
val iconSize = 24.dp // default icon size
assertThat(labelPosition.value?.x).isWithin(1f).of(
(ExpectedPadding + IconPadding + iconSize).toPx()
)
}
}
@Test
fun testOutlinedTextField_labelPositionX_initial_withNullTrailingAndLeading() {
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
label = {
Text(
text = "label",
modifier = Modifier.onGloballyPositioned {
labelPosition.value = it.positionInRoot()
}
)
},
trailingIcon = null,
leadingIcon = null
)
}
rule.runOnIdleWithDensity {
assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
}
}
@Test
fun testOutlinedTextField_colorInLeadingTrailing_whenValidInput() {
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
isError = false,
leadingIcon = {
assertThat(LocalContentColor.current)
.isEqualTo(MaterialTheme.colorScheme.onSurfaceVariant)
},
trailingIcon = {
assertThat(LocalContentColor.current)
.isEqualTo(MaterialTheme.colorScheme.onSurfaceVariant)
}
)
}
}
@Test
fun testOutlinedTextField_colorInLeadingTrailing_whenInvalidInput() {
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
isError = true,
leadingIcon = {
assertThat(LocalContentColor.current)
.isEqualTo(MaterialTheme.colorScheme.onSurfaceVariant)
},
trailingIcon = {
assertThat(LocalContentColor.current).isEqualTo(MaterialTheme.colorScheme.error)
}
)
}
}
@Test
fun testOutlinedTextField_supportingText_position() {
val supportingPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
textStyle = TextStyle(fontSize = 1.sp), // ensure text size is minimum
supportingText = {
Box(Modifier
.size(MinSupportingTextLineHeight)
.onGloballyPositioned {
supportingPosition.value = it.positionInRoot()
}
)
}
)
}
rule.runOnIdleWithDensity {
assertThat(supportingPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
assertThat(supportingPosition.value?.y).isWithin(1f).of(
(ExpectedMinimumTextFieldHeight + SupportingTopPadding).toPx()
)
}
}
@Test
fun testOutlinedTextField_supportingText_contributesToTextFieldMeasurements() {
val tfSize = Ref<IntSize>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier.onGloballyPositioned {
tfSize.value = it.size
},
supportingText = { Text("Supporting") }
)
}
rule.runOnIdleWithDensity {
assertThat(tfSize.value!!.height).isGreaterThan(
ExpectedMinimumTextFieldHeight.roundToPx()
)
}
}
@Test
fun testOutlinedTextField_supportingText_clickFocusesTextField() {
var focused = false
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
modifier = Modifier.onFocusChanged { focused = it.isFocused },
value = "input",
onValueChange = {},
supportingText = { Text("Supporting") }
)
}
rule.onNodeWithText("Supporting").performClick()
rule.runOnIdle {
assertThat(focused).isTrue()
}
}
@Test
fun testOutlinedTextField_supportingText_colorAndStyle() {
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
supportingText = {
assertThat(LocalTextStyle.current)
.isEqualTo(MaterialTheme.typography.bodySmall)
assertThat(LocalContentColor.current)
.isEqualTo(MaterialTheme.colorScheme.onSurfaceVariant)
}
)
}
}
@Test
fun testOutlinedTextField_supportingText_error_colorAndStyle() {
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
isError = true,
supportingText = {
assertThat(LocalTextStyle.current)
.isEqualTo(MaterialTheme.typography.bodySmall)
assertThat(LocalContentColor.current)
.isEqualTo(MaterialTheme.colorScheme.error)
}
)
}
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
fun testOutlinedTextField_imeActionAndKeyboardTypePropagatedDownstream() {
val platformTextInputService = mock<PlatformTextInputService>()
val textInputService = TextInputService(platformTextInputService)
rule.setContent {
CompositionLocalProvider(
LocalTextInputService provides textInputService
) {
val text = remember { mutableStateOf("") }
OutlinedTextField(
modifier = Modifier.testTag(TextFieldTag),
value = text.value,
onValueChange = { text.value = it },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Go,
keyboardType = KeyboardType.Email
)
)
}
}
rule.onNodeWithTag(TextFieldTag).performClick()
rule.runOnIdle {
verify(platformTextInputService, atLeastOnce()).startInput(
value = any(),
imeOptions = eq(
ImeOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Go
)
),
onEditCommand = any(),
onImeActionPerformed = any()
)
}
}
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun testOutlinedTextField_visualTransformationPropagated() {
rule.setMaterialContent(lightColorScheme()) {
Box(Modifier.background(color = Color.White)) {
OutlinedTextField(
modifier = Modifier.testTag(TextFieldTag),
value = "qwerty",
onValueChange = {},
visualTransformation = PasswordVisualTransformation('\u0020')
)
}
}
rule.onNodeWithTag(TextFieldTag)
.captureToImage()
.assertShape(
density = rule.density,
backgroundColor = Color.White,
shapeColor = Color.White,
shape = RectangleShape,
// avoid elevation artifacts
shapeOverlapPixelCount = with(rule.density) { 3.dp.toPx() }
)
}
@Test
fun testOutlinedTextField_errorSemantics_defaultMessage() {
lateinit var errorMessage: String
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "test",
onValueChange = {},
isError = true
)
errorMessage = getString(Strings.DefaultErrorMessage)
}
rule.onNodeWithText("test")
.assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Error))
.assert(
SemanticsMatcher.expectValue(SemanticsProperties.Error, errorMessage)
)
}
@Test
fun testOutlinedTextField_errorSemantics_messageOverridable() {
val errorMessage = "Special symbols not allowed"
rule.setMaterialContent(lightColorScheme()) {
val isError = remember { mutableStateOf(true) }
OutlinedTextField(
value = "test",
onValueChange = {},
modifier = Modifier.semantics { if (isError.value) error(errorMessage) },
isError = isError.value
)
}
rule.onNodeWithText("test")
.assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Error))
.assert(SemanticsMatcher.expectValue(SemanticsProperties.Error, errorMessage))
}
@Test
fun testOutlinedTextField_withLabel_doesNotCrash_rowHeightWithMinIntrinsics() {
var size: IntSize? = null
var dividerSize: IntSize? = null
rule.setMaterialContent(lightColorScheme()) {
val text = remember { mutableStateOf("") }
Box(Modifier.onGloballyPositioned { size = it.size }) {
Row(Modifier.height(IntrinsicSize.Min)) {
Divider(
modifier = Modifier
.fillMaxHeight()
.width(10.dp)
.onGloballyPositioned { dividerSize = it.size }
)
OutlinedTextField(
value = text.value,
label = { Text(text = "Label") },
onValueChange = { text.value = it }
)
}
}
}
rule.runOnIdle {
assertThat(dividerSize).isNotNull()
assertThat(size).isNotNull()
assertThat(dividerSize!!.height).isEqualTo(size!!.height)
}
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun testOutlinedTextField_appliesContainerColor() {
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier.testTag(TextFieldTag),
colors = TextFieldDefaults.outlinedTextFieldColors(
containerColor = Color.Red,
unfocusedBorderColor = Color.Red
),
shape = RectangleShape
)
}
rule.onNodeWithTag(TextFieldTag).captureToImage().assertPixels {
Color.Red
}
}
@Test
fun testOutlinedTextField_withLabel_doesNotCrash_columnWidthWithMinIntrinsics() {
var textFieldSize: IntSize? = null
var dividerSize: IntSize? = null
rule.setMaterialContent(lightColorScheme()) {
val text = remember { mutableStateOf("") }
Box {
Column(Modifier.width(IntrinsicSize.Min)) {
Divider(
modifier = Modifier
.fillMaxWidth()
.height(10.dp)
.onGloballyPositioned { dividerSize = it.size }
)
OutlinedTextField(
value = text.value,
label = { Text(text = "Label") },
onValueChange = { text.value = it },
modifier = Modifier.onGloballyPositioned { textFieldSize = it.size }
)
}
}
}
rule.runOnIdle {
assertThat(dividerSize).isNotNull()
assertThat(textFieldSize).isNotNull()
assertThat(dividerSize!!.width).isEqualTo(textFieldSize!!.width)
}
}
@Test
fun testOutlinedTextField_labelStyle() {
val unfocusedLabelColor = Color.Blue
val focusedLabelColor = Color.Red
var textStyle = TextStyle()
var contentColor = Color.Unspecified
val focusRequester = FocusRequester()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
label = {
textStyle = LocalTextStyle.current
contentColor = LocalContentColor.current
},
modifier = Modifier.focusRequester(focusRequester),
colors = TextFieldDefaults.textFieldColors(
unfocusedLabelColor = unfocusedLabelColor,
focusedLabelColor = focusedLabelColor
)
)
}
rule.runOnIdle {
assertThat(contentColor).isEqualTo(unfocusedLabelColor)
assertThat(textStyle.color).isEqualTo(Color.Unspecified)
}
rule.runOnUiThread {
focusRequester.requestFocus()
}
rule.runOnIdle {
assertThat(contentColor).isEqualTo(focusedLabelColor)
assertThat(textStyle.color).isEqualTo(Color.Unspecified)
}
}
@Test
fun testOutlinedTextField_labelStyle_whenBodySmallStyleColorProvided() {
val unfocusedLabelColor = Color.Blue
val focusedLabelColor = Color.Red
val bodySmallColor = Color.Green
var textStyle = TextStyle()
var contentColor = Color.Unspecified
val focusRequester = FocusRequester()
rule.setMaterialContent(lightColorScheme()) {
val bodySmall = MaterialTheme.typography.bodySmall.copy(color = bodySmallColor)
MaterialTheme(typography = Typography(bodySmall = bodySmall)) {
OutlinedTextField(
value = "",
onValueChange = {},
label = {
textStyle = LocalTextStyle.current
contentColor = LocalContentColor.current
},
modifier = Modifier.focusRequester(focusRequester),
colors = TextFieldDefaults.textFieldColors(
unfocusedLabelColor = unfocusedLabelColor,
focusedLabelColor = focusedLabelColor
)
)
}
}
rule.runOnIdle {
assertThat(textStyle.color).isEqualTo(unfocusedLabelColor)
assertThat(contentColor).isEqualTo(unfocusedLabelColor)
}
rule.runOnUiThread {
focusRequester.requestFocus()
}
rule.runOnIdle {
assertThat(textStyle.color).isEqualTo(bodySmallColor)
assertThat(contentColor).isEqualTo(focusedLabelColor)
}
}
@Test
fun testOutlinedTextField_labelStyle_middle_whenBodySmallStyleColorProvided() {
val expectedLabelColor = Color.Blue
val focusedLabelColor = Color.Red
var textStyle = TextStyle()
var contentColor = Color.Unspecified
val focusRequester = FocusRequester()
rule.mainClock.autoAdvance = false
rule.setMaterialContent(lightColorScheme()) {
val bodySmall = MaterialTheme.typography.bodySmall.copy(color = expectedLabelColor)
MaterialTheme(typography = Typography(bodySmall = bodySmall)) {
OutlinedTextField(
value = "",
onValueChange = {},
label = {
textStyle = LocalTextStyle.current
contentColor = LocalContentColor.current
},
modifier = Modifier.focusRequester(focusRequester),
colors = TextFieldDefaults.textFieldColors(
unfocusedLabelColor = expectedLabelColor,
focusedLabelColor = focusedLabelColor
)
)
}
}
rule.runOnUiThread {
focusRequester.requestFocus()
}
// animation duration is 150, advancing by 75 to get into middle of animation
rule.mainClock.advanceTimeBy(75)
rule.runOnIdle {
assertThat(textStyle.color).isEqualTo(expectedLabelColor)
// color should be a lerp between 'start' and 'end' colors. We check here that it's
// not equal to either of them
assertThat(contentColor).isNotEqualTo(expectedLabelColor)
assertThat(contentColor).isNotEqualTo(focusedLabelColor)
}
}
@Test
fun testOutlinedTextField_labelStyle_whenBothTypographiesColorProvided() {
val unfocusedLabelColor = Color.Blue
val focusedLabelColor = Color.Red
val bodySmallColor = Color.Green
val bodyLargeColor = Color.Black
var textStyle = TextStyle()
var contentColor = Color.Unspecified
val focusRequester = FocusRequester()
rule.setMaterialContent(lightColorScheme()) {
val bodySmall = MaterialTheme.typography.bodySmall.copy(color = bodySmallColor)
val bodyLarge = MaterialTheme.typography.bodyLarge.copy(color = bodyLargeColor)
MaterialTheme(typography = Typography(bodySmall = bodySmall, bodyLarge = bodyLarge)) {
OutlinedTextField(
value = "",
onValueChange = {},
label = {
textStyle = LocalTextStyle.current
contentColor = LocalContentColor.current
},
modifier = Modifier.focusRequester(focusRequester),
colors = TextFieldDefaults.textFieldColors(
unfocusedLabelColor = unfocusedLabelColor,
focusedLabelColor = focusedLabelColor
)
)
}
}
rule.runOnIdle {
assertThat(textStyle.color).isEqualTo(bodyLargeColor)
assertThat(contentColor).isEqualTo(unfocusedLabelColor)
}
rule.runOnUiThread {
focusRequester.requestFocus()
}
rule.runOnIdle {
assertThat(textStyle.color).isEqualTo(bodySmallColor)
assertThat(contentColor).isEqualTo(focusedLabelColor)
}
}
@Test
fun testOutlinedTextField_withIntrinsicsMeasurement_getsIdle() {
rule.setMaterialContent(lightColorScheme()) {
val text = remember { mutableStateOf("") }
Row(Modifier.height(IntrinsicSize.Min)) {
OutlinedTextField(
value = text.value,
onValueChange = { text.value = it },
label = { Text("Label") }
)
Divider(Modifier.fillMaxHeight())
}
}
rule.onNodeWithText("Label")
.assertExists()
.assertIsDisplayed()
.performTextInput("text")
rule.onNodeWithText("text").assertExists()
}
@Test
fun testOutlinedTextField_intrinsicsMeasurement_correctHeight() {
var height = 0
rule.setMaterialContent(lightColorScheme()) {
val text = remember { mutableStateOf("") }
Box(Modifier.onGloballyPositioned {
height = it.size.height
}) {
Row(Modifier.height(IntrinsicSize.Min)) {
OutlinedTextField(
value = text.value,
onValueChange = { text.value = it },
textStyle = TextStyle(fontSize = 1.sp), // ensure text size is minimum
)
Divider(Modifier.fillMaxHeight())
}
}
}
with(rule.density) {
assertThat(height).isEqualTo((TextFieldDefaults.MinHeight).roundToPx())
}
}
@Test
fun testOutlinedTextField_intrinsicsMeasurement_withLeadingIcon_correctHeight() {
var height = 0
rule.setMaterialContent(lightColorScheme()) {
val text = remember { mutableStateOf("") }
Box(Modifier.onGloballyPositioned {
height = it.size.height
}) {
Row(Modifier.height(IntrinsicSize.Min)) {
OutlinedTextField(
value = text.value,
onValueChange = { text.value = it },
textStyle = TextStyle(fontSize = 1.sp), // ensure text size is minimum
leadingIcon = { Icon(Icons.Default.Favorite, null) }
)
Divider(Modifier.fillMaxHeight())
}
}
}
with(rule.density) {
assertThat(height).isEqualTo((TextFieldDefaults.MinHeight).roundToPx())
}
}
@Test
fun testOutlinedTextField_intrinsicsMeasurement_withTrailingIcon_correctHeight() {
var height = 0
rule.setMaterialContent(lightColorScheme()) {
val text = remember { mutableStateOf("") }
Box(Modifier.onGloballyPositioned {
height = it.size.height
}) {
Row(Modifier.height(IntrinsicSize.Min)) {
OutlinedTextField(
value = text.value,
onValueChange = { text.value = it },
textStyle = TextStyle(fontSize = 1.sp), // ensure text size is minimum
trailingIcon = { Icon(Icons.Default.Favorite, null) }
)
Divider(Modifier.fillMaxHeight())
}
}
}
with(rule.density) {
assertThat(height).isEqualTo((TextFieldDefaults.MinHeight).roundToPx())
}
}
@Test
fun outlinedTextField_stringOverload_doesNotCallOnValueChange_whenCompositionUpdatesOnly() {
var callbackCounter = 0
rule.setContent {
val focusManager = LocalFocusManager.current
val text = remember { mutableStateOf("A") }
OutlinedTextField(
value = text.value,
onValueChange = {
callbackCounter += 1
text.value = it
// causes TextFieldValue's composition clearing
focusManager.clearFocus(true)
},
modifier = Modifier.testTag(TextFieldTag)
)
}
rule.onNodeWithTag(TextFieldTag)
.performClick()
rule.waitForIdle()
rule.onNodeWithTag(TextFieldTag)
.performTextClearance()
rule.runOnIdle {
assertThat(callbackCounter).isEqualTo(1)
}
}
private fun getLabelPosition(labelHeight: Int): Int {
val labelHalfHeight = labelHeight / 2
val paddingTop = with(rule.density) { OutlinedTextFieldTopPadding.toPx() }
// Vertical position is the default padding - half height.
// This can be negative, meaning default padding is not enough for the focused label.
return (paddingTop - labelHalfHeight).roundToInt()
}
}