[go: nahoru, domu]

blob: c7f16efdab26a46de222c97a67aa97f5e1348ad5 [file] [log] [blame]
/*
* Copyright 2019 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.foundation
import android.os.Handler
import android.os.Looper
import androidx.annotation.RequiresApi
import androidx.compose.animation.core.FloatExponentialDecaySpec
import androidx.compose.animation.core.ManualAnimationClock
import androidx.compose.foundation.animation.FlingConfig
import androidx.compose.foundation.animation.scrollBy
import androidx.compose.foundation.animation.smoothScrollBy
import androidx.compose.foundation.gestures.Scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Providers
import androidx.compose.runtime.mutableStateOf
import androidx.compose.testutils.MockAnimationClock
import androidx.compose.testutils.assertPixels
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.GestureScope
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.center
import androidx.compose.ui.test.down
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performGesture
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.swipeDown
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.swipeRight
import androidx.compose.ui.test.swipeUp
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.LargeTest
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@MediumTest
@RunWith(AndroidJUnit4::class)
class ScrollTest {
@get:Rule
val rule = createComposeRule()
private val scrollerTag = "ScrollerTest"
private val defaultCrossAxisSize = 45
private val defaultMainAxisSize = 40
private val defaultCellSize = 5
private val colors = listOf(
Color(red = 0xFF, green = 0, blue = 0, alpha = 0xFF),
Color(red = 0xFF, green = 0xA5, blue = 0, alpha = 0xFF),
Color(red = 0xFF, green = 0xFF, blue = 0, alpha = 0xFF),
Color(red = 0xA5, green = 0xFF, blue = 0, alpha = 0xFF),
Color(red = 0, green = 0xFF, blue = 0, alpha = 0xFF),
Color(red = 0, green = 0xFF, blue = 0xA5, alpha = 0xFF),
Color(red = 0, green = 0, blue = 0xFF, alpha = 0xFF),
Color(red = 0xA5, green = 0, blue = 0xFF, alpha = 0xFF)
)
@Before
fun before() {
isDebugInspectorInfoEnabled = true
}
@After
fun after() {
isDebugInspectorInfoEnabled = false
}
@SdkSuppress(minSdkVersion = 26)
@Test
fun verticalScroller_SmallContent() {
val height = 40
composeVerticalScroller(height = height)
validateVerticalScroller(height = height)
}
@Test
fun verticalScroller_SmallContent_Unscrollable() {
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
)
composeVerticalScroller(scrollState)
rule.runOnIdle {
assertTrue(scrollState.maxValue == 0f)
}
}
@SdkSuppress(minSdkVersion = 26)
@Test
fun verticalScroller_LargeContent_NoScroll() {
val height = 30
composeVerticalScroller(height = height)
validateVerticalScroller(height = height)
}
@OptIn(ExperimentalTestApi::class)
@SdkSuppress(minSdkVersion = 26)
@Test
fun verticalScroller_LargeContent_ScrollToEnd() = runBlocking {
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
)
val height = 30
val scrollDistance = 10
composeVerticalScroller(scrollState, height = height)
validateVerticalScroller(height = height)
rule.awaitIdle()
assertEquals(scrollDistance.toFloat(), scrollState.maxValue)
scrollState.scrollTo(scrollDistance.toFloat())
rule.runOnIdle {} // Just so the block below is correct
validateVerticalScroller(offset = scrollDistance, height = height)
}
@SdkSuppress(minSdkVersion = 26)
@Test
fun verticalScroller_Reversed() {
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
)
val height = 30
val expectedOffset = defaultCellSize * colors.size - height
composeVerticalScroller(scrollState, height = height, isReversed = true)
validateVerticalScroller(offset = expectedOffset, height = height)
}
@OptIn(ExperimentalTestApi::class)
@SdkSuppress(minSdkVersion = 26)
@Test
fun verticalScroller_LargeContent_Reversed_ScrollToEnd() = runBlocking {
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
)
val height = 20
val scrollDistance = 10
val expectedOffset = defaultCellSize * colors.size - height - scrollDistance
composeVerticalScroller(scrollState, height = height, isReversed = true)
rule.awaitIdle()
scrollState.scrollTo(scrollDistance.toFloat())
rule.awaitIdle()
validateVerticalScroller(offset = expectedOffset, height = height)
}
@SdkSuppress(minSdkVersion = 26)
@Test
fun horizontalScroller_SmallContent() {
val width = 40
composeHorizontalScroller(width = width)
validateHorizontalScroller(width = width)
}
@SdkSuppress(minSdkVersion = 26)
@Test
fun horizontalScroller_rtl_SmallContent() {
val width = 40
composeHorizontalScroller(width = width, isRtl = true)
validateHorizontalScroller(width = width, checkInRtl = true)
}
@SdkSuppress(minSdkVersion = 26)
@Test
fun horizontalScroller_LargeContent_NoScroll() {
val width = 30
composeHorizontalScroller(width = width)
validateHorizontalScroller(width = width)
}
@SdkSuppress(minSdkVersion = 26)
@Test
fun horizontalScroller_rtl_LargeContent_NoScroll() {
val width = 30
composeHorizontalScroller(width = width, isRtl = true)
validateHorizontalScroller(width = width, checkInRtl = true)
}
@OptIn(ExperimentalTestApi::class)
@SdkSuppress(minSdkVersion = 26)
@Test
fun horizontalScroller_LargeContent_ScrollToEnd() = runBlocking {
val width = 30
val scrollDistance = 10
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
)
composeHorizontalScroller(scrollState, width = width)
validateHorizontalScroller(width = width)
rule.awaitIdle()
assertEquals(scrollDistance.toFloat(), scrollState.maxValue)
scrollState.scrollTo(scrollDistance.toFloat())
rule.runOnIdle {} // Just so the block below is correct
validateHorizontalScroller(offset = scrollDistance, width = width)
}
@OptIn(ExperimentalTestApi::class)
@SdkSuppress(minSdkVersion = 26)
@Test
fun horizontalScroller_rtl_LargeContent_ScrollToEnd() = runBlocking {
val width = 30
val scrollDistance = 10
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
)
composeHorizontalScroller(scrollState, width = width, isRtl = true)
validateHorizontalScroller(width = width, checkInRtl = true)
rule.awaitIdle()
assertEquals(scrollDistance.toFloat(), scrollState.maxValue)
scrollState.scrollTo(scrollDistance.toFloat())
rule.awaitIdle()
validateHorizontalScroller(offset = scrollDistance, width = width, checkInRtl = true)
}
@SdkSuppress(minSdkVersion = 26)
@Test
fun horizontalScroller_reversed() {
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
)
val width = 30
val expectedOffset = defaultCellSize * colors.size - width
composeHorizontalScroller(scrollState, width = width, isReversed = true)
validateHorizontalScroller(offset = expectedOffset, width = width)
}
@SdkSuppress(minSdkVersion = 26)
@Test
fun horizontalScroller_rtl_reversed() {
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
)
val width = 30
val expectedOffset = defaultCellSize * colors.size - width
composeHorizontalScroller(scrollState, width = width, isReversed = true, isRtl = true)
validateHorizontalScroller(offset = expectedOffset, width = width, checkInRtl = true)
}
@OptIn(ExperimentalTestApi::class)
@SdkSuppress(minSdkVersion = 26)
@Test
fun horizontalScroller_LargeContent_Reversed_ScrollToEnd() = runBlocking {
val width = 30
val scrollDistance = 10
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
)
val expectedOffset = defaultCellSize * colors.size - width - scrollDistance
composeHorizontalScroller(scrollState, width = width, isReversed = true)
rule.awaitIdle()
scrollState.scrollTo(scrollDistance.toFloat())
rule.runOnIdle {} // Just so the block below is correct
validateHorizontalScroller(offset = expectedOffset, width = width)
}
@OptIn(ExperimentalTestApi::class)
@SdkSuppress(minSdkVersion = 26)
@Test
fun horizontalScroller_rtl_LargeContent_Reversed_ScrollToEnd() = runBlocking {
val width = 30
val scrollDistance = 10
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
)
val expectedOffset = defaultCellSize * colors.size - width - scrollDistance
composeHorizontalScroller(scrollState, width = width, isReversed = true, isRtl = true)
rule.awaitIdle()
scrollState.scrollTo(scrollDistance.toFloat())
rule.runOnIdle {} // Just so the block below is correct
validateHorizontalScroller(offset = expectedOffset, width = width, checkInRtl = true)
}
@Test
fun verticalScroller_scrollTo_scrollForward() {
createScrollableContent(isVertical = true)
rule.onNodeWithText("50")
.assertIsNotDisplayed()
.performScrollTo()
.assertIsDisplayed()
}
@Test
fun horizontalScroller_scrollTo_scrollForward() {
createScrollableContent(isVertical = false)
rule.onNodeWithText("50")
.assertIsNotDisplayed()
.performScrollTo()
.assertIsDisplayed()
}
@Ignore("Unignore when b/156389287 is fixed for proper reverse and rtl delegation")
@Test
fun horizontalScroller_rtl_scrollTo_scrollForward() {
createScrollableContent(isVertical = false, isRtl = true)
rule.onNodeWithText("50")
.assertIsNotDisplayed()
.performScrollTo()
.assertIsDisplayed()
}
@Ignore("Unignore when b/156389287 is fixed for proper reverse delegation")
@Test
fun verticalScroller_reversed_scrollTo_scrollForward() {
createScrollableContent(
isVertical = true,
scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
),
isReversed = true
)
rule.onNodeWithText("50")
.assertIsNotDisplayed()
.performScrollTo()
.assertIsDisplayed()
}
@Ignore("Unignore when b/156389287 is fixed for proper reverse and rtl delegation")
@Test
fun horizontalScroller_reversed_scrollTo_scrollForward() {
createScrollableContent(
isVertical = false,
scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
),
isReversed = true
)
rule.onNodeWithText("50")
.assertIsNotDisplayed()
.performScrollTo()
.assertIsDisplayed()
}
@Test
@Ignore("When b/157687898 is fixed, performScrollTo must be adjusted to use semantic bounds")
fun verticalScroller_scrollTo_scrollBack() {
createScrollableContent(isVertical = true)
rule.onNodeWithText("50")
.assertIsNotDisplayed()
.performScrollTo()
.assertIsDisplayed()
rule.onNodeWithText("20")
.assertIsNotDisplayed()
.performScrollTo()
.assertIsDisplayed()
}
@Test
@Ignore("When b/157687898 is fixed, performScrollTo must be adjusted to use semantic bounds")
fun horizontalScroller_scrollTo_scrollBack() {
createScrollableContent(isVertical = false)
rule.onNodeWithText("50")
.assertIsNotDisplayed()
.performScrollTo()
.assertIsDisplayed()
rule.onNodeWithText("20")
.assertIsNotDisplayed()
.performScrollTo()
.assertIsDisplayed()
}
@Test
@LargeTest
fun verticalScroller_swipeUp_swipeDown() {
swipeScrollerAndBack(true, GestureScope::swipeUp, GestureScope::swipeDown)
}
@Test
@LargeTest
fun horizontalScroller_swipeLeft_swipeRight() {
swipeScrollerAndBack(false, GestureScope::swipeLeft, GestureScope::swipeRight)
}
@Test
@LargeTest
fun horizontalScroller_rtl_swipeLeft_swipeRight() {
swipeScrollerAndBack(
false,
GestureScope::swipeRight,
GestureScope::swipeLeft,
isRtl = true
)
}
@OptIn(ExperimentalTestApi::class)
@Test
fun scroller_coerce_whenScrollTo() = runBlocking {
val clock = ManualAnimationClock(0)
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = clock
)
createScrollableContent(isVertical = true, scrollState = scrollState)
rule.awaitIdle()
assertThat(scrollState.value).isEqualTo(0f)
assertThat(scrollState.maxValue).isGreaterThan(0f)
scrollState.scrollTo(-100f)
assertThat(scrollState.value).isEqualTo(0f)
(scrollState as Scrollable).scrollBy(-100f)
assertThat(scrollState.value).isEqualTo(0f)
scrollState.scrollTo(scrollState.maxValue)
assertThat(scrollState.value).isEqualTo(scrollState.maxValue)
scrollState.scrollTo(scrollState.maxValue + 1000)
assertThat(scrollState.value).isEqualTo(scrollState.maxValue)
(scrollState as Scrollable).scrollBy(100f)
assertThat(scrollState.value).isEqualTo(scrollState.maxValue)
}
@OptIn(ExperimentalTestApi::class)
@Test
fun verticalScroller_LargeContent_coerceWhenMaxChanges() = runBlocking {
val clock = ManualAnimationClock(0)
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = clock
)
val itemCount = mutableStateOf(100)
rule.setContent {
Box {
Column(
Modifier
.preferredSize(100.dp)
.testTag(scrollerTag)
.verticalScroll(scrollState)
) {
for (i in 0..itemCount.value) {
BasicText(i.toString())
}
}
}
}
rule.awaitIdle()
assertThat(scrollState.value).isEqualTo(0f)
assertThat(scrollState.maxValue).isGreaterThan(0f)
val max = scrollState.maxValue
scrollState.scrollTo(max)
itemCount.value -= 2
rule.awaitIdle()
val newMax = scrollState.maxValue
assertThat(newMax).isLessThan(max)
assertThat(scrollState.value).isEqualTo(newMax)
}
@OptIn(ExperimentalTestApi::class)
@Test
fun scroller_coerce_whenScrollSmoothTo() = runBlocking {
val clock = ManualAnimationClock(0)
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = clock
)
createScrollableContent(isVertical = true, scrollState = scrollState)
rule.awaitIdle()
assertThat(scrollState.value).isEqualTo(0f)
assertThat(scrollState.maxValue).isGreaterThan(0f)
val max = scrollState.maxValue
scrollState.smoothScrollTo(-100f)
assertThat(scrollState.value).isEqualTo(0f)
(scrollState as Scrollable).smoothScrollBy(-100f)
assertThat(scrollState.value).isEqualTo(0f)
scrollState.smoothScrollTo(scrollState.maxValue)
assertThat(scrollState.value).isEqualTo(max)
scrollState.smoothScrollTo(scrollState.maxValue + 1000)
assertThat(scrollState.value).isEqualTo(max)
(scrollState as Scrollable).smoothScrollBy(100f)
assertThat(scrollState.value).isEqualTo(max)
}
@Test
fun scroller_whenFling_stopsByTouchDown() {
val clock = ManualAnimationClock(0)
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = clock
)
createScrollableContent(isVertical = true, scrollState = scrollState)
rule.runOnIdle {
assertThat(scrollState.value).isEqualTo(0f)
assertThat(scrollState.isAnimationRunning).isEqualTo(false)
}
rule.onNodeWithTag(scrollerTag)
.performGesture { swipeUp() }
rule.runOnIdle {
clock.clockTimeMillis += 100
assertThat(scrollState.isAnimationRunning).isEqualTo(true)
}
rule.onNodeWithTag(scrollerTag)
.performGesture { down(center) }
rule.runOnIdle {
assertThat(scrollState.isAnimationRunning).isEqualTo(false)
}
}
@OptIn(ExperimentalTestApi::class)
@Test
fun scroller_restoresScrollerPosition() = runBlocking {
val restorationTester = StateRestorationTester(rule)
var scrollState: ScrollState? = null
restorationTester.setContent {
scrollState = rememberScrollState()
Column(Modifier.verticalScroll(scrollState!!)) {
repeat(50) {
Box(Modifier.preferredHeight(100.dp))
}
}
}
rule.awaitIdle()
scrollState!!.scrollTo(70f)
scrollState = null
restorationTester.emulateSavedInstanceStateRestore()
rule.runOnIdle {
assertThat(scrollState!!.value).isEqualTo(70f)
}
}
private fun swipeScrollerAndBack(
isVertical: Boolean,
firstSwipe: GestureScope.() -> Unit,
secondSwipe: GestureScope.() -> Unit,
isRtl: Boolean = false
) {
val clock = ManualAnimationClock(0)
val scrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = clock
)
createScrollableContent(isVertical, scrollState = scrollState, isRtl = isRtl)
rule.runOnIdle {
assertThat(scrollState.value).isEqualTo(0f)
}
rule.onNodeWithTag(scrollerTag)
.performGesture { firstSwipe() }
rule.runOnIdle {
clock.clockTimeMillis += 5000
}
rule.onNodeWithTag(scrollerTag)
.awaitScrollAnimation(scrollState)
val scrolledValue = rule.runOnIdle {
scrollState.value
}
assertThat(scrolledValue).isGreaterThan(0f)
rule.onNodeWithTag(scrollerTag)
.performGesture { secondSwipe() }
rule.runOnIdle {
clock.clockTimeMillis += 5000
}
rule.onNodeWithTag(scrollerTag)
.awaitScrollAnimation(scrollState)
rule.runOnIdle {
assertThat(scrollState.value).isLessThan(scrolledValue)
}
}
private fun composeVerticalScroller(
scrollState: ScrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
),
isReversed: Boolean = false,
width: Int = defaultCrossAxisSize,
height: Int = defaultMainAxisSize,
rowHeight: Int = defaultCellSize
) {
// We assume that the height of the device is more than 45 px
with(rule.density) {
rule.setContent {
Box {
Column(
modifier = Modifier
.preferredSize(width.toDp(), height.toDp())
.testTag(scrollerTag)
.verticalScroll(scrollState, reverseScrolling = isReversed)
) {
colors.forEach { color ->
Box(
Modifier
.preferredSize(width.toDp(), rowHeight.toDp())
.background(color)
)
}
}
}
}
}
}
private fun composeHorizontalScroller(
scrollState: ScrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
),
isReversed: Boolean = false,
width: Int = defaultMainAxisSize,
height: Int = defaultCrossAxisSize,
isRtl: Boolean = false
) {
// We assume that the height of the device is more than 45 px
with(rule.density) {
rule.setContent {
val direction = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
Providers(LocalLayoutDirection provides direction) {
Box {
Row(
modifier = Modifier
.preferredSize(width.toDp(), height.toDp())
.testTag(scrollerTag)
.horizontalScroll(scrollState, reverseScrolling = isReversed)
) {
colors.forEach { color ->
Box(
Modifier
.preferredSize(defaultCellSize.toDp(), height.toDp())
.background(color)
)
}
}
}
}
}
}
}
@RequiresApi(api = 26)
private fun validateVerticalScroller(
offset: Int = 0,
width: Int = 45,
height: Int = 40,
rowHeight: Int = 5
) {
rule.onNodeWithTag(scrollerTag)
.captureToImage()
.assertPixels(expectedSize = IntSize(width, height)) { pos ->
val colorIndex = (offset + pos.y) / rowHeight
colors[colorIndex]
}
}
@RequiresApi(api = 26)
private fun validateHorizontalScroller(
offset: Int = 0,
width: Int = 40,
height: Int = 45,
checkInRtl: Boolean = false
) {
val scrollerWidth = colors.size * defaultCellSize
val absoluteOffset = if (checkInRtl) scrollerWidth - width - offset else offset
rule.onNodeWithTag(scrollerTag)
.captureToImage()
.assertPixels(expectedSize = IntSize(width, height)) { pos ->
val colorIndex = (absoluteOffset + pos.x) / defaultCellSize
if (checkInRtl) colors[colors.size - 1 - colorIndex] else colors[colorIndex]
}
}
private fun createScrollableContent(
isVertical: Boolean,
itemCount: Int = 100,
width: Dp = 100.dp,
height: Dp = 100.dp,
isReversed: Boolean = false,
scrollState: ScrollState = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = ManualAnimationClock(0)
),
isRtl: Boolean = false
) {
rule.setContent {
val content = @Composable {
repeat(itemCount) {
BasicText(text = "$it")
}
}
Box {
Box(
Modifier.preferredSize(width, height).background(Color.White)
) {
if (isVertical) {
Column(
Modifier
.testTag(scrollerTag)
.verticalScroll(scrollState, reverseScrolling = isReversed)
) {
content()
}
} else {
val direction = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
Providers(LocalLayoutDirection provides direction) {
Row(
Modifier.testTag(scrollerTag)
.horizontalScroll(scrollState, reverseScrolling = isReversed)
) {
content()
}
}
}
}
}
}
}
// TODO(b/147291885): This should not be needed in the future.
private fun SemanticsNodeInteraction.awaitScrollAnimation(
scroller: ScrollState
): SemanticsNodeInteraction {
val latch = CountDownLatch(1)
val handler = Handler(Looper.getMainLooper())
handler.post(object : Runnable {
override fun run() {
if (scroller.isAnimationRunning) {
handler.post(this)
} else {
latch.countDown()
}
}
})
assertWithMessage("Scroll didn't finish after 20 seconds")
.that(latch.await(20, TimeUnit.SECONDS)).isTrue()
return this
}
@Test
fun testInspectorValue() {
val state = ScrollState(
initial = 0f,
flingConfig = FlingConfig(FloatExponentialDecaySpec()),
animationClock = MockAnimationClock()
)
rule.setContent {
val modifier = Modifier.verticalScroll(state) as InspectableValue
assertThat(modifier.nameFallback).isEqualTo("scroll")
assertThat(modifier.valueOverride).isNull()
assertThat(modifier.inspectableElements.map { it.name }.asIterable()).containsExactly(
"state",
"reverseScrolling",
"isScrollable",
"isVertical"
)
}
}
}