[go: nahoru, domu]

blob: 1431c613946a45fede3a637c5909c40f7bba5f1a [file] [log] [blame]
/*
* Copyright 2020 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 androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
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.graphics.RectangleShape
import androidx.compose.ui.input.mouse.MouseScrollEvent
import androidx.compose.ui.input.mouse.MouseScrollUnit
import androidx.compose.ui.input.mouse.MouseScrollOrientation
import androidx.compose.ui.platform.DesktopPlatform
import androidx.compose.ui.platform.DesktopPlatformAmbient
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.down
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.DesktopComposeTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performGesture
import androidx.compose.ui.test.swipe
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.jetbrains.skija.Surface
import org.junit.Assert.assertEquals
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@Suppress("WrapUnaryOperator")
@OptIn(ExperimentalTestApi::class)
class ScrollbarTest {
@get:Rule
val rule = createComposeRule()
// don't inline, surface controls canvas life time
private val surface = Surface.makeRasterN32Premul(100, 100)
private val canvas = surface.canvas
@Test
fun `drag slider to the middle`() {
runBlocking(Dispatchers.Main) {
rule.setContent {
TestBox(size = 100.dp, childSize = 20.dp, childCount = 10, scrollbarWidth = 10.dp)
}
rule.awaitIdle()
rule.onNodeWithTag("scrollbar").performGesture {
swipe(start = Offset(0f, 25f), end = Offset(0f, 50f))
}
rule.awaitIdle()
rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-50.dp)
}
}
@Test
fun `drag slider to the edges`() {
runBlocking(Dispatchers.Main) {
rule.setContent {
TestBox(size = 100.dp, childSize = 20.dp, childCount = 10, scrollbarWidth = 10.dp)
}
rule.awaitIdle()
rule.onNodeWithTag("scrollbar").performGesture {
swipe(start = Offset(0f, 25f), end = Offset(0f, 500f))
}
rule.awaitIdle()
rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-100.dp)
rule.onNodeWithTag("scrollbar").performGesture {
swipe(start = Offset(0f, 99f), end = Offset(0f, -500f))
}
rule.awaitIdle()
rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(0.dp)
}
}
@Test
fun `drag outside slider`() {
runBlocking(Dispatchers.Main) {
rule.setContent {
TestBox(size = 100.dp, childSize = 20.dp, childCount = 10, scrollbarWidth = 10.dp)
}
rule.awaitIdle()
rule.onNodeWithTag("scrollbar").performGesture {
swipe(start = Offset(10f, 25f), end = Offset(0f, 50f))
}
rule.awaitIdle()
rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(0.dp)
}
}
// TODO(demin): enable after we resolve b/171889442
@Ignore("Enable after we resolve b/171889442")
@Test
fun `mouseScroll over slider`() {
runBlocking(Dispatchers.Main) {
rule.setContent {
TestBox(size = 100.dp, childSize = 20.dp, childCount = 10, scrollbarWidth = 10.dp)
}
rule.awaitIdle()
rule.performMouseScroll(0, 25, 1f)
rule.awaitIdle()
rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-10.dp)
}
}
// TODO(demin): enable after we resolve b/171889442
@Ignore("Enable after we resolve b/171889442")
@Test
fun `mouseScroll over scrollbar outside slider`() {
runBlocking(Dispatchers.Main) {
rule.setContent {
TestBox(size = 100.dp, childSize = 20.dp, childCount = 10, scrollbarWidth = 10.dp)
}
rule.awaitIdle()
rule.performMouseScroll(0, 99, 1f)
rule.awaitIdle()
rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-10.dp)
}
}
// TODO(demin): enable after we resolve b/171889442
@Ignore("Enable after we resolve b/171889442")
@Test
fun `vertical mouseScroll over horizontal scrollbar `() {
runBlocking(Dispatchers.Main) {
// TODO(demin): write tests for vertical mouse scrolling over
// horizontalScrollbar for the case when we have two-way scrollable content:
// Modifier.verticalScrollbar(...).horizontalScrollbar(...)
// Content should scroll vertically.
}
}
@Test
fun `mouseScroll over column then drag to the beginning`() {
runBlocking(Dispatchers.Main) {
rule.setContent {
TestBox(size = 100.dp, childSize = 20.dp, childCount = 10, scrollbarWidth = 10.dp)
}
rule.awaitIdle()
rule.performMouseScroll(20, 25, 10f)
rule.awaitIdle()
rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-100.dp)
rule.onNodeWithTag("scrollbar").performGesture {
swipe(start = Offset(0f, 99f), end = Offset(0f, -500f))
}
rule.awaitIdle()
rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(0.dp)
}
}
@Test(timeout = 3000)
fun `press on scrollbar outside slider`() {
runBlocking(Dispatchers.Main) {
rule.setContent {
TestBox(size = 100.dp, childSize = 20.dp, childCount = 20, scrollbarWidth = 10.dp)
}
rule.awaitIdle()
rule.onNodeWithTag("scrollbar").performGesture {
down(Offset(0f, 26f))
}
tryUntilSucceeded {
rule.awaitIdle()
rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-100.dp)
}
}
}
@Test(timeout = 3000)
fun `press on the end of scrollbar outside slider`() {
runBlocking(Dispatchers.Main) {
rule.setContent {
TestBox(size = 100.dp, childSize = 20.dp, childCount = 20, scrollbarWidth = 10.dp)
}
rule.awaitIdle()
rule.onNodeWithTag("scrollbar").performGesture {
down(Offset(0f, 99f))
}
tryUntilSucceeded {
rule.awaitIdle()
rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-300.dp)
}
}
}
@Test(timeout = 3000)
fun `dynamically change content then drag slider to the end`() {
runBlocking(Dispatchers.Main) {
val isContentVisible = mutableStateOf(false)
rule.setContent {
TestBox(
size = 100.dp,
scrollbarWidth = 10.dp
) {
if (isContentVisible.value) {
repeat(10) {
Box(Modifier.size(20.dp).testTag("box$it"))
}
}
}
}
rule.awaitIdle()
isContentVisible.value = true
rule.awaitIdle()
rule.onNodeWithTag("scrollbar").performGesture {
swipe(start = Offset(0f, 25f), end = Offset(0f, 500f))
}
rule.awaitIdle()
rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-100.dp)
}
}
@Suppress("SameParameterValue")
@OptIn(ExperimentalFoundationApi::class)
@Test(timeout = 3000)
fun `scroll by less than one page in lazy list`() {
runBlocking(Dispatchers.Main) {
lateinit var state: LazyListState
rule.setContent {
state = rememberLazyListState()
LazyTestBox(
state,
size = 100.dp,
childSize = 20.dp,
childCount = 20,
scrollbarWidth = 10.dp
)
}
rule.awaitIdle()
rule.onNodeWithTag("scrollbar").performGesture {
swipe(start = Offset(0f, 0f), end = Offset(0f, 11f), durationMillis = 1)
}
rule.awaitIdle()
assertEquals(2, state.firstVisibleItemIndex)
assertEquals(4, state.firstVisibleItemScrollOffset)
}
}
@Suppress("SameParameterValue")
@OptIn(ExperimentalFoundationApi::class)
@Test(timeout = 3000)
fun `scroll by more than one page in lazy list`() {
runBlocking(Dispatchers.Main) {
lateinit var state: LazyListState
rule.setContent {
state = rememberLazyListState()
LazyTestBox(
state,
size = 100.dp,
childSize = 20.dp,
childCount = 20,
scrollbarWidth = 10.dp
)
}
rule.awaitIdle()
rule.onNodeWithTag("scrollbar").performGesture {
swipe(start = Offset(0f, 0f), end = Offset(0f, 26f), durationMillis = 1)
}
rule.awaitIdle()
assertEquals(5, state.firstVisibleItemIndex)
assertEquals(4, state.firstVisibleItemScrollOffset)
}
}
@Suppress("SameParameterValue")
@OptIn(ExperimentalFoundationApi::class)
@Test(timeout = 3000)
fun `scroll outside of scrollbar bounds in lazy list`() {
runBlocking(Dispatchers.Main) {
lateinit var state: LazyListState
rule.setContent {
state = rememberLazyListState()
LazyTestBox(
state,
size = 100.dp,
childSize = 20.dp,
childCount = 20,
scrollbarWidth = 10.dp
)
}
rule.awaitIdle()
rule.onNodeWithTag("scrollbar").performGesture {
swipe(start = Offset(0f, 0f), end = Offset(0f, 10000f), durationMillis = 1)
}
rule.awaitIdle()
assertEquals(15, state.firstVisibleItemIndex)
assertEquals(0, state.firstVisibleItemScrollOffset)
rule.onNodeWithTag("scrollbar").performGesture {
swipe(start = Offset(0f, 99f), end = Offset(0f, -10000f), durationMillis = 1)
}
rule.awaitIdle()
assertEquals(0, state.firstVisibleItemIndex)
assertEquals(0, state.firstVisibleItemScrollOffset)
}
}
private suspend fun tryUntilSucceeded(block: suspend () -> Unit) {
while (true) {
try {
block()
break
} catch (e: Throwable) {
delay(10)
}
}
}
private fun ComposeTestRule.performMouseScroll(x: Int, y: Int, delta: Float) {
(this as DesktopComposeTestRule).window.onMouseScroll(
x, y, MouseScrollEvent(MouseScrollUnit.Line(delta), MouseScrollOrientation.Vertical)
)
}
@Composable
private fun TestBox(
size: Dp,
childSize: Dp,
childCount: Int,
scrollbarWidth: Dp,
) = withTestEnvironment {
Box(Modifier.size(size)) {
val state = rememberScrollState()
Column(
Modifier.fillMaxSize().testTag("column").verticalScroll(state)
) {
repeat(childCount) {
Box(Modifier.size(childSize).testTag("box$it"))
}
}
VerticalScrollbar(
adapter = rememberScrollbarAdapter(state),
modifier = Modifier
.width(scrollbarWidth)
.fillMaxHeight()
.testTag("scrollbar")
)
}
}
@Composable
private fun TestBox(
size: Dp,
scrollbarWidth: Dp,
scrollableContent: @Composable ColumnScope.() -> Unit
) = withTestEnvironment {
Box(Modifier.size(size)) {
val state = rememberScrollState()
Column(
Modifier.fillMaxSize().testTag("column").verticalScroll(state),
content = scrollableContent
)
VerticalScrollbar(
adapter = rememberScrollbarAdapter(state),
modifier = Modifier
.width(scrollbarWidth)
.fillMaxHeight()
.testTag("scrollbar")
)
}
}
@Suppress("SameParameterValue")
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun LazyTestBox(
state: LazyListState,
size: Dp,
childSize: Dp,
childCount: Int,
scrollbarWidth: Dp,
) = withTestEnvironment {
Box(Modifier.size(size)) {
LazyColumn(
Modifier.fillMaxSize().testTag("column"),
state
) {
items((0 until childCount).toList()) {
Box(Modifier.size(childSize).testTag("box$it"))
}
}
VerticalScrollbar(
adapter = rememberScrollbarAdapter(state, childCount, childSize),
modifier = Modifier
.width(scrollbarWidth)
.fillMaxHeight()
.testTag("scrollbar")
)
}
}
@Composable
private fun withTestEnvironment(content: @Composable () -> Unit) = CompositionLocalProvider(
ScrollbarStyleAmbient provides ScrollbarStyle(
minimalHeight = 16.dp,
thickness = 8.dp,
shape = RectangleShape,
hoverDurationMillis = 300,
unhoverColor = Color.Black,
hoverColor = Color.Red
),
DesktopPlatformAmbient provides DesktopPlatform.MacOS,
content = content
)
}