blob: dd43f7639149ad860e0664bda40a7fca43395ddd [file] [log] [blame]
/*
* Copyright 2021 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.material
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.test.assertRangeInfoEquals
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
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.onNodeWithTag
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.height
import com.google.common.truth.Truth
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@ExperimentalWearMaterialApi
class StepperTest {
@get:Rule
val rule = createComposeRule()
@Test
fun supports_testtag() {
rule.setContentWithTheme {
Stepper(
value = 1f,
onValueChange = {},
steps = 5,
modifier = Modifier.testTag(TEST_TAG)
) {}
}
rule.onNodeWithTag(TEST_TAG).assertExists()
}
@Test
fun coerces_value_top_limit() = rule.setNewValueAndCheck(
range = 0f..10f,
steps = 4,
initialValue = 4f,
newValue = 20f,
expectedValue = 10f
)
@Test
fun coerces_value_lower_limit() = rule.setNewValueAndCheck(
range = 0f..10f,
steps = 4,
initialValue = 4f,
newValue = -20f,
expectedValue = 0f
)
@Test
fun coerces_value_top_limit_and_doesnt_change_state() {
val state = mutableStateOf(4f)
val valueRange = 0f..10f
rule.initDefaultStepper(state, valueRange, 4)
rule.runOnIdle {
state.value = 20f
}
assertEquals(20f, state.value)
}
@Test
fun coerces_value_lower_limit_and_doesnt_change_state() {
val state = mutableStateOf(4f)
val valueRange = 0f..10f
rule.initDefaultStepper(state, valueRange, 4)
rule.runOnIdle {
state.value = -20f
}
assertEquals(-20f, state.value)
}
@Test(expected = IllegalArgumentException::class)
fun throws_when_steps_negative() {
rule.setContent {
Stepper(value = 0f, onValueChange = {}, steps = -1) {}
}
}
@Test
fun snaps_value_exactly() = rule.setNewValueAndCheck(
range = 0f..1f,
steps = 4,
initialValue = 0f,
// Allowed values are only 0, 0.2, 0.4, 0.6, 0.8, 1
newValue = 0.6f,
expectedValue = 0.6f
)
@Test
fun snaps_value_to_previous() = rule.setNewValueAndCheck(
range = 0f..1f,
steps = 4,
initialValue = 0f,
// Allowed values are only 0, 0.2, 0.4, 0.6, 0.8, 1
newValue = 0.65f,
expectedValue = 0.6f
)
@Test
fun snaps_value_to_next() = rule.setNewValueAndCheck(
range = 0f..1f,
steps = 4,
initialValue = 0f,
// Allowed values are only 0, 0.2, 0.4, 0.6, 0.8, 1
newValue = 0.55f,
expectedValue = 0.6f
)
@Test
fun snaps_value_to_next_and_does_not_change_state() {
val state = mutableStateOf(0f)
val valueRange = 0f..1f
rule.initDefaultStepper(state, valueRange, 4)
rule.runOnIdle {
state.value = 0.55f
}
assertEquals(0.55f, state.value)
}
@Test
fun decreases_value_by_clicking_bottom() {
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
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
fun reaches_min_clicking_bottom() {
val state = mutableStateOf(1f)
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
fun reaches_max_clicking_top() {
val state = mutableStateOf(4f)
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
fun sets_custom_decrease_icon() {
val iconTag = "iconTag_test"
rule.setContentWithTheme {
Stepper(
modifier = Modifier.testTag(TEST_TAG),
value = 0f,
steps = 5,
onValueChange = { },
decreaseIcon = {
Icon(
modifier = Modifier.testTag(iconTag),
imageVector = Icons.Default.Star,
contentDescription = ""
)
}
) {}
}
val unclippedBoundsInRoot = rule.onRoot().getUnclippedBoundsInRoot()
rule.waitForIdle()
rule.onNodeWithTag(iconTag, true)
.assertExists()
.assertTopPositionInRootIsEqualTo(
unclippedBoundsInRoot.height -
BorderVerticalMargin - DefaultIconHeight
)
}
@Test
fun sets_custom_increase_icon() {
val iconTag = "iconTag_test"
rule.setContentWithTheme {
Stepper(
modifier = Modifier.testTag(TEST_TAG),
value = 0f,
steps = 5,
onValueChange = { },
increaseIcon = {
Icon(
modifier = Modifier.testTag(iconTag),
imageVector = Icons.Default.Star,
contentDescription = ""
)
}
) {}
}
rule.waitForIdle()
rule.onNodeWithTag(iconTag, true)
.assertExists()
.assertTopPositionInRootIsEqualTo(BorderVerticalMargin)
}
@Test
fun sets_content() {
val contentTag = "contentTag_test"
rule.setContentWithTheme {
Stepper(
modifier = Modifier.testTag(TEST_TAG),
value = 0f,
steps = 5,
onValueChange = { },
) {
Text("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
)
}
private val BorderVerticalMargin = 8.dp
private val VerticalMargin = 8.dp
private val ButtonWeight = .35f
private val DefaultIconHeight = 24.dp
}
private fun ComposeContentTestRule.setNewValueAndCheck(
range: ClosedFloatingPointRange<Float>,
steps: Int,
initialValue: Float,
newValue: Float,
expectedValue: Float
) {
val state = mutableStateOf(initialValue)
initDefaultStepper(state, range, steps)
runOnIdle { state.value = newValue }
onNodeWithTag(TEST_TAG)
.assertRangeInfoEquals(ProgressBarRangeInfo(expectedValue, range, steps))
}
private fun ComposeContentTestRule.initDefaultStepper(
state: MutableState<Float>,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int
) {
setContentWithTheme {
Stepper(
value = state.value,
onValueChange = { state.value = it },
valueRange = valueRange,
steps = steps,
modifier = Modifier.testTag(TEST_TAG)
) {}
}
}