blob: 9dbbad50c114276b0b8ba3075f82164dec757672 [file] [log] [blame]
/*
* Copyright 2023 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.wear.compose.materialcore
import android.os.Build
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.ProvidedValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertContentDescriptionContains
import androidx.compose.ui.test.assertHasNoClickAction
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.click
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onChild
import androidx.compose.ui.test.onChildAt
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onParent
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.height
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
public class StepperTest {
@get:Rule
public val rule = createComposeRule()
@Test
public fun supports_testtag() {
rule.setContent {
StepperWithDefaults(
modifier = Modifier.testTag(TEST_TAG)
) {}
}
rule.onNodeWithTag(TEST_TAG).assertExists()
}
@Test(expected = IllegalArgumentException::class)
public fun throws_when_steps_negative() {
rule.setContent {
StepperWithDefaults(
steps = -1
) {}
}
}
@Test
public fun decreases_value_by_clicking_bottom() {
val state = mutableStateOf(2f)
val range = 1f..4f
rule.initDefaultStepper(state, range, 2)
rule.onNodeWithContentDescription(DECREASE).performClick()
rule.runOnIdle {
Truth.assertThat(state.value).isWithin(0.001f).of(1f)
}
}
@Test
public fun increases_value_by_clicking_top() {
val state = mutableStateOf(2f)
val range = 1f..4f
rule.initDefaultStepper(state, range, 2)
// The clickable area for an increase button takes top 35% of the screen
rule.onNodeWithTag(TEST_TAG).performTouchInput { click(Offset(width / 2f, 15f)) }
rule.runOnIdle {
Truth.assertThat(state.value).isWithin(0.001f).of(3f)
}
}
@Test
public fun reaches_min_clicking_bottom() {
// Start one step above the minimum.
val state = mutableStateOf(2f)
val range = 1f..4f
rule.initDefaultStepper(state, range, 2)
// The clickable area for a decrease button takes bottom 35% of the screen
rule.onNodeWithTag(TEST_TAG).performTouchInput { click(Offset(width / 2f, height - 15f)) }
rule.runOnIdle {
Truth.assertThat(state.value).isWithin(0.001f).of(1f)
}
}
@Test
public fun reaches_max_clicking_top() {
// Start one step below the maximum.
val state = mutableStateOf(3f)
val range = 1f..4f
rule.initDefaultStepper(state, range, 2)
// The clickable area for an increase button takes top 35% of the screen
rule.onNodeWithTag(TEST_TAG).performTouchInput { click(Offset(width / 2f, 15f)) }
rule.runOnIdle {
Truth.assertThat(state.value).isWithin(0.001f).of(4f)
}
}
@Test
public fun disables_decrease_when_minimum_value_reached() {
val state = mutableStateOf(1f)
val range = 1f..4f
rule.initDefaultStepper(state, range, 2)
rule.onNodeWithContentDescription(DECREASE).onParent().assertHasNoClickAction()
}
@Test
public fun disables_increase_when_maximum_value_reached() {
val state = mutableStateOf(4f)
val range = 1f..4f
rule.initDefaultStepper(state, range, 2)
rule.onNodeWithContentDescription(INCREASE).onParent().assertHasNoClickAction()
}
@Test
public fun checks_decrease_icon_position() {
rule.setContent {
StepperWithDefaults(
modifier = Modifier.testTag(TEST_TAG)
) {}
}
val unclippedBoundsInRoot = rule.onRoot().getUnclippedBoundsInRoot()
rule.waitForIdle()
rule.onNodeWithTag(DECREASE, true)
.assertExists()
.assertTopPositionInRootIsEqualTo(
unclippedBoundsInRoot.height -
BorderVerticalMargin - DefaultIconHeight
)
}
@Test
public fun sets_custom_decrease_icon() {
val iconTag = "iconTag_test"
rule.setContent {
StepperWithDefaults(
modifier = Modifier.testTag(TEST_TAG),
decreaseIcon = {
TestImage(Modifier.size(24.dp), iconTag)
}
) {}
}
rule.waitForIdle()
rule.onNodeWithTag(iconTag, true)
.assertExists()
}
@Test
public fun sets_custom_increase_icon() {
val iconTag = "iconTag_test"
rule.setContent {
StepperWithDefaults(
modifier = Modifier.testTag(TEST_TAG),
increaseIcon = {
TestImage(Modifier.size(24.dp), iconTag)
}
) {}
}
rule.waitForIdle()
rule.onNodeWithTag(iconTag, true)
.assertExists()
.assertTopPositionInRootIsEqualTo(BorderVerticalMargin)
}
@Test
public fun sets_content() {
val contentTag = "contentTag_test"
rule.setContent {
StepperWithDefaults(
modifier = Modifier.testTag(TEST_TAG),
) {
TestText(
"Testing",
modifier = Modifier
.testTag(contentTag)
.fillMaxHeight()
)
}
}
val rootHeight = rule.onRoot().getUnclippedBoundsInRoot().height
rule.waitForIdle()
rule.onNodeWithTag(contentTag, true)
.assertExists()
.assertTopPositionInRootIsEqualTo(
// Position of the content is a weight(35%) of (top button minus 2 spacers 8dp each)
// plus 1 spacer
(rootHeight - VerticalMargin * 2) * ButtonWeight + VerticalMargin
)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
public fun sets_background_color() {
val testColor = Color.Blue
val backgroundThreshold = 50.0f
rule.setContent {
StepperWithDefaults(
modifier = Modifier.testTag(TEST_TAG),
backgroundColor = testColor
) {}
}
rule.onNodeWithTag(TEST_TAG)
.captureToImage()
.assertContainsColor(testColor, backgroundThreshold)
}
@Test
public fun sets_custom_description_for_increase_icon() {
val testContentDescription = "testContentDescription"
rule.setContent {
StepperWithDefaults(
modifier = Modifier.testTag(TEST_TAG),
value = 0f,
steps = 5,
increaseIcon = {
TestImage(Modifier.size(24.dp), testContentDescription)
}
) {}
}
rule.waitForIdle()
rule.onNodeWithTag(TEST_TAG, true)
// 0 is the index of increase button, 1 - decrease button, content is empty
.onChildAt(0)
.onChild()
.assertContentDescriptionContains(testContentDescription)
}
@Test
public fun sets_custom_description_for_decrease_icon() {
val testContentDescription = "testContentDescription"
rule.setContent {
StepperWithDefaults(
modifier = Modifier.testTag(TEST_TAG),
value = 0f,
steps = 5,
decreaseIcon = {
TestImage(Modifier.size(24.dp), testContentDescription)
}
) {}
}
rule.waitForIdle()
rule.onNodeWithTag(TEST_TAG, true)
// 0 is the index of increase button, 1 - decrease button, content is empty
.onChildAt(1)
.onChild()
.assertContentDescriptionContains(testContentDescription)
}
@Test
public fun sets_button_provider_values_correctly_for_minimum_value() {
verifyButtonProviderValues(
value = 1f,
valueRange = 1f..4f,
steps = 3,
expectedIncreaseIconData = +1,
expectedDecreaseIconData = -1,
enabledButtonProviderValues = arrayOf(
LocalContentTestData provides +1
),
disabledButtonProviderValues = arrayOf(
LocalContentTestData provides -1
)
)
}
@Test
public fun sets_button_provider_values_correctly_for_maximum_value() {
verifyButtonProviderValues(
value = 4f,
valueRange = 1f..4f,
steps = 3,
expectedIncreaseIconData = -1,
expectedDecreaseIconData = +1,
enabledButtonProviderValues = arrayOf(
LocalContentTestData provides +1
),
disabledButtonProviderValues = arrayOf(
LocalContentTestData provides -1
)
)
}
@Test
public fun sets_button_provider_values_correctly_for_value_between_min_and_max() {
verifyButtonProviderValues(
value = 2f,
valueRange = 1f..4f,
steps = 3,
expectedIncreaseIconData = +1,
expectedDecreaseIconData = +1,
enabledButtonProviderValues = arrayOf(
LocalContentTestData provides +1
),
disabledButtonProviderValues = arrayOf(
LocalContentTestData provides -1
)
)
}
private val BorderVerticalMargin = 22.dp
private val VerticalMargin = 8.dp
private val ButtonWeight = .35f
private val DefaultIconHeight = 24.dp
@Composable
internal fun StepperWithDefaults(
modifier: Modifier = Modifier,
value: Float = 1f,
onValueChange: (Float) -> Unit = {},
steps: Int = 5,
decreaseIcon: @Composable () -> Unit = {
TestImage(
modifier = Modifier.size(24.dp),
iconLabel = DECREASE
)
},
increaseIcon: @Composable () -> Unit = {
TestImage(
modifier = Modifier.size(24.dp),
iconLabel = INCREASE
)
},
valueRange: ClosedFloatingPointRange<Float> = 0f..5f,
backgroundColor: Color = Color.Black,
enabledButtonProviderValues: Array<ProvidedValue<*>> = arrayOf(),
disabledButtonProviderValues: Array<ProvidedValue<*>> = arrayOf(),
content: @Composable BoxScope.() -> Unit
) = Stepper(
modifier = modifier,
value = value,
onValueChange = onValueChange,
steps = steps,
increaseIcon = increaseIcon,
decreaseIcon = decreaseIcon,
valueRange = valueRange,
backgroundColor = backgroundColor,
enabledButtonProviderValues = enabledButtonProviderValues,
disabledButtonProviderValues = disabledButtonProviderValues,
buttonRipple = EmptyIndication,
content = content,
)
private fun verifyButtonProviderValues(
value: Float,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int,
expectedIncreaseIconData: Int,
expectedDecreaseIconData: Int,
enabledButtonProviderValues: Array<ProvidedValue<*>> = arrayOf(),
disabledButtonProviderValues: Array<ProvidedValue<*>> = arrayOf()
) {
var increaseIconData = 0
var decreaseIconData = 0
rule.setContent {
StepperWithDefaults(
modifier = Modifier.testTag(TEST_TAG),
value = value,
steps = steps,
valueRange = valueRange,
backgroundColor = Color.Transparent,
enabledButtonProviderValues = enabledButtonProviderValues,
disabledButtonProviderValues = disabledButtonProviderValues,
decreaseIcon = {
decreaseIconData = LocalContentTestData.current
},
increaseIcon = {
increaseIconData = LocalContentTestData.current
}
) {}
}
Assert.assertEquals(increaseIconData, expectedIncreaseIconData)
Assert.assertEquals(decreaseIconData, expectedDecreaseIconData)
}
private fun ComposeContentTestRule.initDefaultStepper(
state: MutableState<Float>,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int
) {
setContent {
StepperWithDefaults(
value = state.value,
onValueChange = { state.value = it },
valueRange = valueRange,
steps = steps,
modifier = Modifier.testTag(TEST_TAG)
) {}
}
}
private val INCREASE = "increase"
private val DECREASE = "decrease"
}