blob: 6f898c9d3eb50ba294dad1fa8ad997bd89353e3d [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.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.TextFieldDefaults.OutlinedTextFieldDecorationBox
import androidx.compose.material3.TextFieldDefaults.TextFieldDecorationBox
import androidx.compose.material3.TextFieldDefaults.indicatorLine
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.testutils.assertPixels
import androidx.compose.ui.Modifier
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.onSizeChanged
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import kotlin.math.roundToInt
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@MediumTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalMaterial3Api::class)
class TextFieldDecorationBoxTest {
@get:Rule
val rule = createComposeRule()
private val Density = Density(1f)
private val LabelHeight = 40.dp
private val InnerTextFieldHeight = 50.dp
private val InnerTextFieldWidth = 100.dp
@Test
fun outlinedTextFieldBox_overrideTopPadding_multiLine() {
assertVerticalSizeAndPosition_outlinedTextField(
padding = TextFieldDefaults.outlinedTextFieldPadding(top = 10.dp),
singleLine = false,
expectedHeight = 10.dp + InnerTextFieldHeight + TextFieldPadding,
expectedPosition = 10.dp
)
}
@Test
fun outlinedTextFieldBox_overrideTopPadding_singleLine() {
assertVerticalSizeAndPosition_outlinedTextField(
padding = TextFieldDefaults.outlinedTextFieldPadding(top = 10.dp),
singleLine = true,
expectedHeight = 10.dp + InnerTextFieldHeight + TextFieldPadding,
expectedPosition = (10.dp + TextFieldPadding) / 2
)
}
@Test
fun outlinedTextFieldBox_overrideBottomPadding_multiLine() {
assertVerticalSizeAndPosition_outlinedTextField(
padding = TextFieldDefaults.outlinedTextFieldPadding(bottom = 10.dp),
singleLine = false,
expectedHeight = TextFieldPadding + InnerTextFieldHeight + 10.dp,
expectedPosition = TextFieldPadding
)
}
@Test
fun outlinedTextFieldBox_overrideBottomPadding_singleLine() {
assertVerticalSizeAndPosition_outlinedTextField(
padding = TextFieldDefaults.outlinedTextFieldPadding(bottom = 10.dp),
singleLine = true,
expectedHeight = TextFieldPadding + InnerTextFieldHeight + 10.dp,
expectedPosition = (10.dp + TextFieldPadding) / 2
)
}
@Test
fun outlinedTextFieldBox_overrideStartPadding() {
assertHorizontalSizeAndPosition_outlinedTextField(
padding = TextFieldDefaults.outlinedTextFieldPadding(start = 10.dp),
rtl = false,
expectedWidth = 10.dp + InnerTextFieldWidth + TextFieldPadding,
expectedPosition = 10.dp
)
}
@Test
fun outlinedTextFieldBox_overrideStartPadding_rtl() {
assertHorizontalSizeAndPosition_outlinedTextField(
padding = TextFieldDefaults.outlinedTextFieldPadding(start = 10.dp),
rtl = true,
expectedWidth = 10.dp + InnerTextFieldWidth + TextFieldPadding,
expectedPosition = TextFieldPadding
)
}
@Test
fun outlinedTextFieldBox_overrideEndPadding() {
assertHorizontalSizeAndPosition_outlinedTextField(
padding = TextFieldDefaults.outlinedTextFieldPadding(end = 20.dp),
rtl = false,
expectedWidth = TextFieldPadding + InnerTextFieldWidth + 20.dp,
expectedPosition = TextFieldPadding
)
}
@Test
fun outlinedTextFieldBox_overrideEndPadding_rtl() {
assertHorizontalSizeAndPosition_outlinedTextField(
padding = TextFieldDefaults.outlinedTextFieldPadding(end = 20.dp),
rtl = true,
expectedWidth = TextFieldPadding + InnerTextFieldWidth + 20.dp,
expectedPosition = 20.dp
)
}
@Test
fun textFieldBox_overrideTopPadding_singleLine_withoutLabel() {
assertVerticalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithoutLabelPadding(top = 40.dp),
singleLine = true,
hasLabel = false,
expectedHeight = 40.dp + InnerTextFieldHeight + TextFieldPadding,
expectedPosition = (40.dp + TextFieldPadding) / 2
)
}
@Test
fun textFieldBox_overrideTopPadding_singleLine_withLabel() {
assertVerticalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithLabelPadding(top = 40.dp),
singleLine = true,
hasLabel = true,
expectedHeight = 40.dp + LabelHeight + InnerTextFieldHeight +
TextFieldWithLabelVerticalPadding,
expectedPosition = 40.dp + LabelHeight
)
}
@Test
fun textFieldBox_overrideBottomPadding_singleLine_withoutLabel() {
assertVerticalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithoutLabelPadding(bottom = 40.dp),
singleLine = true,
hasLabel = false,
expectedHeight = TextFieldPadding + InnerTextFieldHeight + 40.dp,
expectedPosition = (TextFieldPadding + 40.dp) / 2
)
}
@Test
fun textFieldBox_overrideBottomPadding_singleLine_withLabel() {
assertVerticalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithLabelPadding(bottom = 40.dp),
singleLine = true,
hasLabel = true,
expectedHeight = TextFieldWithLabelVerticalPadding + LabelHeight +
InnerTextFieldHeight + 40.dp,
expectedPosition = TextFieldWithLabelVerticalPadding + LabelHeight
)
}
@Test
fun textFieldBox_overrideTopPadding_multiLine_withoutLabel() {
assertVerticalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithoutLabelPadding(top = 40.dp),
singleLine = false,
hasLabel = false,
expectedHeight = 40.dp + InnerTextFieldHeight + TextFieldPadding,
expectedPosition = 40.dp
)
}
@Test
fun textFieldBox_overrideTopPadding_multiLine_withLabel() {
assertVerticalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithLabelPadding(top = 40.dp),
singleLine = false,
hasLabel = true,
expectedHeight = 40.dp + LabelHeight + InnerTextFieldHeight +
TextFieldWithLabelVerticalPadding,
expectedPosition = 40.dp + LabelHeight
)
}
@Test
fun textFieldBox_overrideBottomPadding_multiLine_withoutLabel() {
assertVerticalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithoutLabelPadding(bottom = 40.dp),
singleLine = false,
hasLabel = false,
expectedHeight = TextFieldPadding + InnerTextFieldHeight + 40.dp,
expectedPosition = TextFieldPadding
)
}
@Test
fun textFieldBox_overrideBottomPadding_multiLine_withLabel() {
assertVerticalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithLabelPadding(bottom = 40.dp),
singleLine = false,
hasLabel = true,
expectedHeight = TextFieldWithLabelVerticalPadding + LabelHeight +
InnerTextFieldHeight + 40.dp,
expectedPosition = TextFieldWithLabelVerticalPadding + LabelHeight
)
}
@Test
fun textFieldBox_overrideStartPadding_withLabel() {
assertHorizontalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithLabelPadding(start = 40.dp),
rtl = false,
hasLabel = true,
expectedWidth = 40.dp + InnerTextFieldWidth + TextFieldPadding,
expectedPosition = 40.dp
)
}
@Test
fun textFieldBox_overrideStartPadding_withLabel_rtl() {
assertHorizontalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithLabelPadding(start = 40.dp),
rtl = true,
hasLabel = true,
expectedWidth = 40.dp + InnerTextFieldWidth + TextFieldPadding,
expectedPosition = TextFieldPadding
)
}
@Test
fun textFieldBox_overrideStartPadding_withoutLabel() {
assertHorizontalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithoutLabelPadding(start = 40.dp),
rtl = false,
hasLabel = false,
expectedWidth = 40.dp + InnerTextFieldWidth + TextFieldPadding,
expectedPosition = 40.dp
)
}
@Test
fun textFieldBox_overrideStartPadding_withoutLabel_rtl() {
assertHorizontalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithoutLabelPadding(start = 40.dp),
rtl = true,
hasLabel = false,
expectedWidth = 40.dp + InnerTextFieldWidth + TextFieldPadding,
expectedPosition = TextFieldPadding
)
}
@Test
fun textFieldBox_overrideEndPadding_withLabel() {
assertHorizontalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithLabelPadding(end = 40.dp),
rtl = false,
hasLabel = true,
expectedWidth = TextFieldPadding + InnerTextFieldWidth + 40.dp,
expectedPosition = TextFieldPadding
)
}
@Test
fun textFieldBox_overrideEndPadding_withLabel_rtl() {
assertHorizontalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithLabelPadding(end = 40.dp),
rtl = true,
hasLabel = true,
expectedWidth = TextFieldPadding + InnerTextFieldWidth + 40.dp,
expectedPosition = 40.dp
)
}
@Test
fun textFieldBox_overrideEndPadding_withoutLabel() {
assertHorizontalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithoutLabelPadding(end = 40.dp),
rtl = false,
hasLabel = false,
expectedWidth = TextFieldPadding + InnerTextFieldWidth + 40.dp,
expectedPosition = TextFieldPadding
)
}
@Test
fun textFieldBox_overrideEndPadding_withoutLabel_rtl() {
assertHorizontalSizeAndPosition_textField(
padding = TextFieldDefaults.textFieldWithoutLabelPadding(end = 40.dp),
rtl = true,
hasLabel = false,
expectedWidth = TextFieldPadding + InnerTextFieldWidth + 40.dp,
expectedPosition = 40.dp
)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun outlinedTextFieldBox_defaultBorderColor_comesFromColors() {
val textFieldWidth = 300
val textFieldHeight = 150
val borderWidth = 40
val value = "Text"
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalDensity provides Density) {
val interactionSource = remember { MutableInteractionSource() }
val singleLine = true
val colors = TextFieldDefaults.outlinedTextFieldColors(
unfocusedBorderColor = Color.Red
)
BasicTextField(
value = value,
onValueChange = {},
modifier = Modifier.size(
with(Density) { textFieldWidth.toDp() },
with(Density) { textFieldHeight.toDp() }
),
singleLine = singleLine,
interactionSource = interactionSource
) {
OutlinedTextFieldDecorationBox(
value = value,
innerTextField = it,
enabled = true,
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
singleLine = singleLine,
container = {
TextFieldDefaults.OutlinedBorderContainerBox(
enabled = true,
isError = false,
colors = colors,
interactionSource = interactionSource,
shape = RectangleShape,
unfocusedBorderThickness = with(Density) { borderWidth.toDp() }
)
},
colors = colors,
contentPadding = PaddingValues(0.dp)
)
}
}
}
rule.onNodeWithText(value)
.captureToImage()
.assertPixels(IntSize(textFieldWidth, textFieldHeight)) {
// to account for edge pixels
if (it.x in 2..(textFieldWidth - 2) && it.y in 2..(borderWidth - 2)) {
Color.Red
} else {
null
}
}
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun textFieldBox_defaultIndicatorLineColor_comesFromColors() {
val textFieldWidth = 300
val textFieldHeight = 150
val borderWidth = 40
val value = "Text"
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalDensity provides Density) {
val interactionSource = remember { MutableInteractionSource() }
val singleLine = true
val colors = TextFieldDefaults.textFieldColors(
unfocusedIndicatorColor = Color.Red
)
BasicTextField(
value = value,
onValueChange = {},
modifier = Modifier
.indicatorLine(enabled = true,
isError = false,
colors = colors,
interactionSource = interactionSource,
unfocusedIndicatorLineThickness = with(Density) { borderWidth.toDp() })
.size(with(Density) { textFieldWidth.toDp() },
with(Density) { textFieldHeight.toDp() }),
singleLine = singleLine,
interactionSource = interactionSource
) {
TextFieldDecorationBox(
value = value,
innerTextField = it,
enabled = true,
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
singleLine = singleLine,
colors = colors,
contentPadding = PaddingValues(0.dp)
)
}
}
}
rule.onNodeWithText(value)
.captureToImage()
.assertPixels(IntSize(textFieldWidth, textFieldHeight)) {
// to account for edge pixels
if (it.x in 2..(textFieldWidth - 2) &&
it.y in (textFieldHeight - borderWidth + 2)..(textFieldHeight - 2)
) {
Color.Red
} else {
null
}
}
}
@Test
fun textFieldBox_overridePadding_unfocusedState_withoutLabel_withPlaceholder() {
val placeholderDimension = 50.dp
val verticalPadding = 10.dp
val value = ""
var size: IntSize? = null
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(LocalDensity provides Density) {
val interactionSource = remember { MutableInteractionSource() }
val singleLine = false
BasicTextField(
value = value,
onValueChange = {},
modifier = Modifier.onSizeChanged { size = it },
singleLine = singleLine,
interactionSource = interactionSource
) {
TextFieldDecorationBox(
value = value,
innerTextField = it,
enabled = true,
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
singleLine = singleLine,
placeholder = { Spacer(Modifier.size(placeholderDimension)) },
contentPadding = PaddingValues(vertical = verticalPadding)
)
}
}
}
rule.runOnIdle {
with(Density) {
assertThat(size).isNotNull()
assertThat(size!!.height).isEqualTo(
(placeholderDimension + verticalPadding * 2).roundToPx())
}
}
}
@Test
fun outlinedTextFieldBox_innerTextLocation_withMultilineLabel() {
assertSizeAndPosition(
padding = TextFieldDefaults.outlinedTextFieldPadding(),
singleLine = false,
expectedSize = LabelHeight / 2 + InnerTextFieldHeight + TextFieldPadding,
expectedPosition = LabelHeight / 2,
isVertical = true,
isOutlined = true,
label = {
// imitates the multiline label
Box(Modifier.size(10.dp, LabelHeight))
}
)
}
@Test
fun outlinedTextFieldBox_singleLine_innerTextLocation_withMultilineLabel() {
assertSizeAndPosition(
padding = TextFieldDefaults.outlinedTextFieldPadding(),
singleLine = true,
expectedSize = LabelHeight / 2 + InnerTextFieldHeight + TextFieldPadding,
expectedPosition = LabelHeight / 2,
isVertical = true,
isOutlined = true,
label = {
// imitates the multiline label
Box(Modifier.size(10.dp, LabelHeight))
}
)
}
private fun assertVerticalSizeAndPosition_outlinedTextField(
padding: PaddingValues,
singleLine: Boolean,
expectedHeight: Dp,
expectedPosition: Dp
) {
assertSizeAndPosition(
padding = padding,
singleLine = singleLine,
expectedSize = expectedHeight,
expectedPosition = expectedPosition,
isVertical = true,
isOutlined = true,
)
}
private fun assertHorizontalSizeAndPosition_outlinedTextField(
padding: PaddingValues,
rtl: Boolean,
expectedWidth: Dp,
expectedPosition: Dp
) {
assertSizeAndPosition(
padding = padding,
singleLine = true,
expectedSize = expectedWidth,
expectedPosition = expectedPosition,
isVertical = false,
isOutlined = true,
layoutDirection = if (rtl) LayoutDirection.Rtl else LayoutDirection.Ltr
)
}
private fun assertVerticalSizeAndPosition_textField(
padding: PaddingValues,
singleLine: Boolean,
hasLabel: Boolean,
expectedHeight: Dp,
expectedPosition: Dp
) {
assertSizeAndPosition(
padding = padding,
singleLine = singleLine,
expectedSize = expectedHeight,
expectedPosition = expectedPosition,
isVertical = true,
isOutlined = false,
label = if (hasLabel) {
{ Text("Label", modifier = Modifier.height(LabelHeight)) }
} else { null },
)
}
private fun assertHorizontalSizeAndPosition_textField(
padding: PaddingValues,
rtl: Boolean,
hasLabel: Boolean,
expectedWidth: Dp,
expectedPosition: Dp
) {
assertSizeAndPosition(
padding = padding,
singleLine = true,
expectedSize = expectedWidth,
expectedPosition = expectedPosition,
isVertical = false,
isOutlined = false,
layoutDirection = if (rtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
label = if (hasLabel) {
{ Text("Label", modifier = Modifier.height(LabelHeight)) }
} else { null },
)
}
private fun assertSizeAndPosition(
padding: PaddingValues,
singleLine: Boolean,
expectedSize: Dp,
expectedPosition: Dp,
isVertical: Boolean,
isOutlined: Boolean,
layoutDirection: LayoutDirection = LayoutDirection.Ltr,
label: @Composable (() -> Unit)? = null,
) {
var size: IntSize? = null
var position: Offset? = null
rule.setMaterialContent(lightColorScheme()) {
CompositionLocalProvider(
LocalLayoutDirection provides layoutDirection,
LocalDensity provides Density
) {
Box(Modifier.onSizeChanged { size = it }) {
val value = "Text"
val interactionSource = remember { MutableInteractionSource() }
BasicTextField(
value = value,
onValueChange = {},
singleLine = singleLine,
interactionSource = interactionSource
) {
val innerTextField: @Composable () -> Unit = {
Box(
Modifier
.size(InnerTextFieldWidth, InnerTextFieldHeight)
.onGloballyPositioned {
position = it.positionInRoot()
}
) { it() }
}
if (isOutlined) {
OutlinedTextFieldDecorationBox(
value = value,
innerTextField = innerTextField,
enabled = true,
singleLine = singleLine,
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
contentPadding = padding,
label = label
)
} else {
TextFieldDecorationBox(
value = value,
innerTextField = innerTextField,
enabled = true,
singleLine = singleLine,
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
contentPadding = padding,
label = label
)
}
}
}
}
}
rule.runOnIdle {
with(Density) {
assertThat(size).isNotNull()
if (isVertical) {
assertThat(size!!.height).isEqualTo(expectedSize.roundToPx())
} else {
assertThat(size!!.width).isEqualTo(expectedSize.roundToPx())
}
assertThat(position).isNotNull()
if (isVertical) {
assertThat(position!!.y.roundToInt()).isEqualTo(expectedPosition.roundToPx())
} else {
assertThat(position!!.x.roundToInt()).isEqualTo(expectedPosition.roundToPx())
}
}
}
}
}