| /* |
| * 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.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.max |
| 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 labelSize = Ref<IntSize>() |
| val labelPosition = Ref<Offset>() |
| rule.setMaterialContent(lightColorScheme()) { |
| Box { |
| OutlinedTextField( |
| value = "", |
| onValueChange = {}, |
| singleLine = true, |
| label = { |
| Text( |
| text = "label", |
| modifier = Modifier.onGloballyPositioned { |
| labelPosition.value = it.positionInRoot() |
| labelSize.value = it.size |
| } |
| ) |
| } |
| ) |
| } |
| } |
| |
| rule.runOnIdleWithDensity { |
| // size |
| assertThat(labelSize.value).isNotNull() |
| assertThat(labelSize.value?.height).isGreaterThan(0) |
| assertThat(labelSize.value?.width).isGreaterThan(0) |
| assertThat(labelPosition.value?.x).isEqualTo( |
| ExpectedPadding.roundToPx().toFloat() |
| ) |
| // label is centered in 56.dp default container, plus additional 8.dp padding on top |
| val minimumHeight = ExpectedMinimumTextFieldHeight.roundToPx() |
| assertThat(labelPosition.value?.y).isEqualTo( |
| ((minimumHeight - labelSize.value!!.height) / 2f).roundToInt() + 8.dp.roundToPx() |
| ) |
| } |
| } |
| |
| @Test |
| fun testOutlinedTextField_labelPosition_initial_withMultiLineLabel() { |
| val textFieldWidth = 200.dp |
| val labelSize = Ref<IntSize>() |
| rule.setMaterialContent(lightColorScheme()) { |
| Box { |
| 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 |
| } |
| ) |
| } |
| ) |
| } |
| } |
| |
| rule.runOnIdleWithDensity { |
| // label size |
| assertThat(labelSize.value).isNotNull() |
| assertThat(labelSize.value?.height).isGreaterThan(0) |
| assertThat(labelSize.value?.width) |
| .isEqualTo(textFieldWidth.roundToPx() - 2 * ExpectedPadding.roundToPx()) |
| } |
| } |
| |
| @Test |
| fun testOutlinedTextField_labelPosition_initial_withDefaultHeight() { |
| val labelSize = Ref<IntSize>() |
| val labelPosition = Ref<Offset>() |
| rule.setMaterialContent(lightColorScheme()) { |
| Box { |
| OutlinedTextField( |
| value = "", |
| onValueChange = {}, |
| label = { |
| Text( |
| text = "label", |
| modifier = Modifier.onGloballyPositioned { |
| labelPosition.value = it.positionInRoot() |
| labelSize.value = it.size |
| } |
| ) |
| } |
| ) |
| } |
| } |
| |
| rule.runOnIdleWithDensity { |
| // size |
| assertThat(labelSize.value).isNotNull() |
| assertThat(labelSize.value?.height).isGreaterThan(0) |
| assertThat(labelSize.value?.width).isGreaterThan(0) |
| assertThat(labelPosition.value?.x).isEqualTo( |
| ExpectedPadding.roundToPx().toFloat() |
| ) |
| // label is aligned to the top with padding, plus additional 8.dp padding on top |
| assertThat(labelPosition.value?.y).isEqualTo( |
| TextFieldPadding.roundToPx() + 8.dp.roundToPx() |
| ) |
| } |
| } |
| |
| @Test |
| fun testOutlinedTextField_labelPosition_whenFocused() { |
| val labelSize = Ref<IntSize>() |
| val labelPosition = Ref<Offset>() |
| |
| rule.setMaterialContent(lightColorScheme()) { |
| OutlinedTextField( |
| modifier = Modifier.testTag(TextFieldTag), |
| value = "", |
| onValueChange = {}, |
| label = { |
| Text( |
| text = "label", |
| modifier = Modifier.onGloballyPositioned { |
| labelPosition.value = it.positionInRoot() |
| labelSize.value = it.size |
| } |
| ) |
| } |
| ) |
| } |
| |
| // click to focus |
| rule.onNodeWithTag(TextFieldTag).performClick() |
| |
| rule.runOnIdleWithDensity { |
| // size |
| assertThat(labelSize.value).isNotNull() |
| assertThat(labelSize.value?.height).isGreaterThan(0) |
| assertThat(labelSize.value?.width).isGreaterThan(0) |
| // label position |
| assertThat(labelPosition.value?.x).isEqualTo( |
| ExpectedPadding.roundToPx().toFloat() |
| ) |
| assertThat(labelPosition.value?.y).isEqualTo(getLabelPosition(labelSize)) |
| } |
| } |
| |
| @Test |
| fun testOutlinedTextField_labelPosition_whenFocused_withMultiLineLabel() { |
| val textFieldWidth = 200.dp |
| val labelSize = Ref<IntSize>() |
| rule.setMaterialContent(lightColorScheme()) { |
| Box { |
| 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 |
| } |
| ) |
| } |
| ) |
| } |
| } |
| |
| // 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) |
| .isEqualTo(textFieldWidth.roundToPx() - 2 * ExpectedPadding.roundToPx()) |
| } |
| } |
| |
| @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 = Ref<IntSize>() |
| val labelPosition = Ref<Offset>() |
| rule.setMaterialContent(lightColorScheme()) { |
| OutlinedTextField( |
| value = "input", |
| onValueChange = {}, |
| label = { |
| Text( |
| text = "label", |
| modifier = Modifier.onGloballyPositioned { |
| labelPosition.value = it.positionInRoot() |
| labelSize.value = it.size |
| } |
| ) |
| } |
| ) |
| } |
| |
| rule.runOnIdleWithDensity { |
| // size |
| assertThat(labelSize.value).isNotNull() |
| assertThat(labelSize.value?.height).isGreaterThan(0) |
| assertThat(labelSize.value?.width).isGreaterThan(0) |
| // label position |
| assertThat(labelPosition.value?.x).isEqualTo( |
| ExpectedPadding.roundToPx().toFloat() |
| ) |
| assertThat(labelPosition.value?.y).isEqualTo(getLabelPosition(labelSize)) |
| } |
| } |
| |
| @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 = Ref<IntSize>() |
| val placeholderPosition = Ref<Offset>() |
| rule.setMaterialContent(lightColorScheme()) { |
| Box { |
| OutlinedTextField( |
| modifier = Modifier.testTag(TextFieldTag), |
| value = "", |
| onValueChange = {}, |
| label = { Text("label") }, |
| placeholder = { |
| Text( |
| text = "placeholder", |
| modifier = Modifier.onGloballyPositioned { |
| placeholderPosition.value = it.positionInRoot() |
| placeholderSize.value = it.size |
| } |
| ) |
| } |
| ) |
| } |
| } |
| // click to focus |
| rule.onNodeWithTag(TextFieldTag).performClick() |
| |
| rule.runOnIdleWithDensity { |
| // size |
| assertThat(placeholderSize.value).isNotNull() |
| assertThat(placeholderSize.value?.height).isGreaterThan(0) |
| assertThat(placeholderSize.value?.width).isGreaterThan(0) |
| // position |
| assertThat(placeholderPosition.value?.x).isEqualTo( |
| ExpectedPadding.roundToPx().toFloat() |
| ) |
| // placeholder is aligned to the top with padding, plus additional 8.dp padding on top |
| assertThat(placeholderPosition.value?.y).isEqualTo( |
| TextFieldPadding.roundToPx() + 8.dp.roundToPx() |
| ) |
| } |
| } |
| |
| @Test |
| fun testOutlinedTextField_placeholderPosition_whenNoLabel() { |
| val placeholderSize = Ref<IntSize>() |
| val placeholderPosition = Ref<Offset>() |
| rule.setMaterialContent(lightColorScheme()) { |
| Box { |
| OutlinedTextField( |
| modifier = Modifier.testTag(TextFieldTag), |
| value = "", |
| 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 { |
| // size |
| assertThat(placeholderSize.value).isNotNull() |
| assertThat(placeholderSize.value?.height).isGreaterThan(0) |
| assertThat(placeholderSize.value?.width).isGreaterThan(0) |
| // position |
| assertThat(placeholderPosition.value?.x).isEqualTo( |
| ExpectedPadding.roundToPx().toFloat() |
| ) |
| // placeholder is placed with fixed padding |
| assertThat(placeholderPosition.value?.y).isEqualTo(TextFieldPadding.roundToPx()) |
| } |
| } |
| |
| @Test |
| fun testOutlinedTextField_noPlaceholder_whenInputNotEmpty() { |
| val placeholderSize = Ref<IntSize>() |
| val placeholderPosition = Ref<Offset>() |
| rule.setMaterialContent(lightColorScheme()) { |
| Column { |
| 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_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 textFieldHeight = 60.dp |
| val labelSize = Ref<IntSize>() |
| val prefixPosition = Ref<Offset>() |
| val suffixPosition = Ref<Offset>() |
| val suffixSize = Ref<IntSize>() |
| val density = Density(2f) |
| |
| rule.setMaterialContent(lightColorScheme()) { |
| CompositionLocalProvider(LocalDensity provides density) { |
| OutlinedTextField( |
| value = "text", |
| onValueChange = {}, |
| modifier = Modifier.size(textFieldWidth, textFieldHeight), |
| label = { |
| Text( |
| text = "label", |
| modifier = Modifier.onGloballyPositioned { |
| labelSize.value = it.size |
| } |
| ) |
| }, |
| prefix = { |
| Text( |
| text = "P", |
| modifier = Modifier.onGloballyPositioned { |
| prefixPosition.value = it.positionInRoot() |
| } |
| ) |
| }, |
| suffix = { |
| Text( |
| text = "S", |
| modifier = Modifier.onGloballyPositioned { |
| suffixPosition.value = it.positionInRoot() |
| suffixSize.value = it.size |
| } |
| ) |
| } |
| ) |
| } |
| } |
| |
| rule.runOnIdle { |
| with(density) { |
| // prefix |
| assertThat(prefixPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx()) |
| assertThat(prefixPosition.value?.y).isWithin(1f).of( |
| (ExpectedPadding + 8.dp).toPx()) |
| |
| // suffix |
| assertThat(suffixPosition.value?.x).isWithin(1f).of( |
| (textFieldWidth - ExpectedPadding - suffixSize.value!!.width.toDp()).toPx() |
| ) |
| assertThat(suffixPosition.value?.y).isWithin(1f).of( |
| (ExpectedPadding + 8.dp).toPx()) |
| } |
| } |
| } |
| |
| @Test |
| fun testOutlinedTextField_prefixAndSuffixPosition_whenNoLabel() { |
| val textFieldWidth = 300.dp |
| val textFieldHeight = 60.dp |
| val prefixPosition = Ref<Offset>() |
| val suffixPosition = Ref<Offset>() |
| val suffixSize = Ref<IntSize>() |
| val density = Density(2f) |
| |
| rule.setMaterialContent(lightColorScheme()) { |
| CompositionLocalProvider(LocalDensity provides density) { |
| OutlinedTextField( |
| value = "text", |
| onValueChange = {}, |
| modifier = Modifier.size(textFieldWidth, textFieldHeight), |
| prefix = { |
| Text( |
| text = "P", |
| modifier = Modifier.onGloballyPositioned { |
| prefixPosition.value = it.positionInRoot() |
| } |
| ) |
| }, |
| suffix = { |
| Text( |
| text = "S", |
| modifier = Modifier.onGloballyPositioned { |
| suffixPosition.value = it.positionInRoot() |
| suffixSize.value = it.size |
| } |
| ) |
| } |
| ) |
| } |
| } |
| |
| 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.value!!.width.toDp()).toPx() |
| ) |
| assertThat(suffixPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx()) |
| } |
| } |
| } |
| |
| @Test |
| fun testOutlinedTextField_prefixAndSuffixPosition_withIcons() { |
| val textFieldWidth = 300.dp |
| val textFieldHeight = 60.dp |
| val prefixPosition = Ref<Offset>() |
| val suffixPosition = Ref<Offset>() |
| val suffixSize = Ref<IntSize>() |
| val density = Density(2f) |
| |
| rule.setMaterialContent(lightColorScheme()) { |
| CompositionLocalProvider(LocalDensity provides density) { |
| OutlinedTextField( |
| value = "text", |
| onValueChange = {}, |
| modifier = Modifier.size(textFieldWidth, textFieldHeight), |
| prefix = { |
| Text( |
| text = "P", |
| modifier = Modifier.onGloballyPositioned { |
| prefixPosition.value = it.positionInRoot() |
| } |
| ) |
| }, |
| suffix = { |
| Text( |
| text = "S", |
| modifier = Modifier.onGloballyPositioned { |
| suffixPosition.value = it.positionInRoot() |
| suffixSize.value = it.size |
| } |
| ) |
| }, |
| 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.value!!.width.toDp()).toPx() |
| ) |
| assertThat(suffixPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx()) |
| } |
| } |
| } |
| |
| @Test |
| fun testOutlinedTextField_labelPositionX_initial_withTrailingAndLeading() { |
| val labelPosition = Ref<Offset>() |
| rule.setMaterialContent(lightColorScheme()) { |
| Box { |
| 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).isEqualTo( |
| (ExpectedPadding + IconPadding + iconSize).roundToPx().toFloat() |
| ) |
| } |
| } |
| |
| @Test |
| fun testOutlinedTextField_labelPositionX_initial_withNullTrailingAndLeading() { |
| val labelPosition = Ref<Offset>() |
| rule.setMaterialContent(lightColorScheme()) { |
| Box { |
| OutlinedTextField( |
| value = "", |
| onValueChange = {}, |
| label = { |
| Text( |
| text = "label", |
| modifier = Modifier.onGloballyPositioned { |
| labelPosition.value = it.positionInRoot() |
| } |
| ) |
| }, |
| trailingIcon = null, |
| leadingIcon = null |
| ) |
| } |
| } |
| |
| rule.runOnIdleWithDensity { |
| assertThat(labelPosition.value?.x).isEqualTo( |
| ExpectedPadding.roundToPx().toFloat() |
| ) |
| } |
| } |
| |
| @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 tfSize = Ref<IntSize>() |
| val supportingSize = Ref<IntSize>() |
| val supportingPosition = Ref<Offset>() |
| rule.setMaterialContent(lightColorScheme()) { |
| OutlinedTextField( |
| value = "", |
| onValueChange = {}, |
| modifier = Modifier.onGloballyPositioned { |
| tfSize.value = it.size |
| }, |
| supportingText = { |
| Text( |
| text = "Supporting", |
| modifier = Modifier.onGloballyPositioned { |
| supportingSize.value = it.size |
| supportingPosition.value = it.positionInRoot() |
| } |
| ) |
| } |
| ) |
| } |
| |
| rule.runOnIdleWithDensity { |
| assertThat(supportingPosition.value?.x).isEqualTo( |
| ExpectedPadding.roundToPx().toFloat() |
| ) |
| assertThat(supportingPosition.value?.y).isEqualTo( |
| tfSize.value!!.height - supportingSize.value!!.height |
| ) |
| } |
| } |
| |
| @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 testErrorSemantics_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 testErrorSemantics_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 }, |
| placeholder = { Text("placeholder") } |
| ) |
| 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 }, |
| placeholder = { Text("placeholder") }, |
| 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 }, |
| placeholder = { Text("placeholder") }, |
| 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(labelSize: Ref<IntSize>): Int { |
| val labelHalfHeight = labelSize.value!!.height / 2 |
| val paddingTop = with(rule.density) { OutlinedTextFieldTopPadding.toPx() } |
| // vertical position is the default padding - half height |
| // in case negative position, fix to 0 |
| return max(paddingTop - labelHalfHeight, 0f).roundToInt() |
| } |
| } |