[go: nahoru, domu]

blob: 1431c613946a45fede3a637c5909c40f7bba5f1a [file] [log] [blame]
Igor Demind7332f82020-10-23 21:45:51 +03001/*
2 * Copyright 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.compose.foundation
18
19import androidx.compose.foundation.layout.Box
Andrey Kulikov05c18b22020-12-14 14:26:44 +000020import androidx.compose.foundation.layout.Column
Igor Demin3bc059e2020-11-25 21:19:35 +030021import androidx.compose.foundation.layout.ColumnScope
Igor Demind7332f82020-10-23 21:45:51 +030022import androidx.compose.foundation.layout.fillMaxHeight
23import androidx.compose.foundation.layout.fillMaxSize
24import androidx.compose.foundation.layout.size
25import androidx.compose.foundation.layout.width
Andrey Kulikov1e8ebd32020-12-08 22:12:16 +000026import androidx.compose.foundation.lazy.LazyColumn
Igor Demind7332f82020-10-23 21:45:51 +030027import androidx.compose.foundation.lazy.LazyListState
Andrey Kulikovbb05cd72020-12-11 19:39:21 +000028import androidx.compose.foundation.lazy.items
Igor Demind7332f82020-10-23 21:45:51 +030029import androidx.compose.foundation.lazy.rememberLazyListState
30import androidx.compose.runtime.Composable
Leland Richardson0f99bf12021-02-02 20:56:41 -080031import androidx.compose.runtime.CompositionLocalProvider
Igor Demin3bc059e2020-11-25 21:19:35 +030032import androidx.compose.runtime.mutableStateOf
Igor Demind7332f82020-10-23 21:45:51 +030033import androidx.compose.ui.Modifier
34import androidx.compose.ui.geometry.Offset
Igor Demind7332f82020-10-23 21:45:51 +030035import androidx.compose.ui.graphics.Color
36import androidx.compose.ui.graphics.RectangleShape
37import androidx.compose.ui.input.mouse.MouseScrollEvent
38import androidx.compose.ui.input.mouse.MouseScrollUnit
Matvei Malkov693e3cc2021-02-05 19:18:12 +000039import androidx.compose.ui.input.mouse.MouseScrollOrientation
Igor Demind7332f82020-10-23 21:45:51 +030040import androidx.compose.ui.platform.DesktopPlatform
41import androidx.compose.ui.platform.DesktopPlatformAmbient
42import androidx.compose.ui.platform.testTag
Jelle Fresen8e55c4f2020-12-16 13:15:53 +000043import androidx.compose.ui.test.ExperimentalTestApi
Igor Demind7332f82020-10-23 21:45:51 +030044import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
45import androidx.compose.ui.test.down
46import androidx.compose.ui.test.junit4.ComposeTestRule
47import androidx.compose.ui.test.junit4.DesktopComposeTestRule
48import androidx.compose.ui.test.junit4.createComposeRule
49import androidx.compose.ui.test.onNodeWithTag
50import androidx.compose.ui.test.performGesture
51import androidx.compose.ui.test.swipe
52import androidx.compose.ui.unit.Dp
53import androidx.compose.ui.unit.dp
Igor Demind7332f82020-10-23 21:45:51 +030054import kotlinx.coroutines.Dispatchers
55import kotlinx.coroutines.delay
56import kotlinx.coroutines.runBlocking
57import org.jetbrains.skija.Surface
58import org.junit.Assert.assertEquals
59import org.junit.Ignore
60import org.junit.Rule
61import org.junit.Test
62
63@Suppress("WrapUnaryOperator")
Jelle Fresen8e55c4f2020-12-16 13:15:53 +000064@OptIn(ExperimentalTestApi::class)
Igor Demind7332f82020-10-23 21:45:51 +030065class ScrollbarTest {
66 @get:Rule
67 val rule = createComposeRule()
Igor Demin67e4aa22020-11-19 11:12:43 +030068
69 // don't inline, surface controls canvas life time
Igor Demind4827cb2021-01-18 15:30:47 +030070 private val surface = Surface.makeRasterN32Premul(100, 100)
Igor Demin67e4aa22020-11-19 11:12:43 +030071 private val canvas = surface.canvas
Igor Demind7332f82020-10-23 21:45:51 +030072
73 @Test
74 fun `drag slider to the middle`() {
75 runBlocking(Dispatchers.Main) {
76 rule.setContent {
77 TestBox(size = 100.dp, childSize = 20.dp, childCount = 10, scrollbarWidth = 10.dp)
78 }
79 rule.awaitIdle()
80
81 rule.onNodeWithTag("scrollbar").performGesture {
82 swipe(start = Offset(0f, 25f), end = Offset(0f, 50f))
83 }
Igor Demin3dfcd042020-12-03 17:08:19 +030084 rule.awaitIdle()
Igor Demind7332f82020-10-23 21:45:51 +030085 rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-50.dp)
86 }
87 }
88
89 @Test
90 fun `drag slider to the edges`() {
91 runBlocking(Dispatchers.Main) {
92 rule.setContent {
93 TestBox(size = 100.dp, childSize = 20.dp, childCount = 10, scrollbarWidth = 10.dp)
94 }
95 rule.awaitIdle()
96
97 rule.onNodeWithTag("scrollbar").performGesture {
98 swipe(start = Offset(0f, 25f), end = Offset(0f, 500f))
99 }
Igor Demin3dfcd042020-12-03 17:08:19 +0300100 rule.awaitIdle()
Igor Demind7332f82020-10-23 21:45:51 +0300101 rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-100.dp)
102
103 rule.onNodeWithTag("scrollbar").performGesture {
104 swipe(start = Offset(0f, 99f), end = Offset(0f, -500f))
105 }
Igor Demin3dfcd042020-12-03 17:08:19 +0300106 rule.awaitIdle()
Igor Demind7332f82020-10-23 21:45:51 +0300107 rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(0.dp)
108 }
109 }
110
111 @Test
112 fun `drag outside slider`() {
113 runBlocking(Dispatchers.Main) {
114 rule.setContent {
115 TestBox(size = 100.dp, childSize = 20.dp, childCount = 10, scrollbarWidth = 10.dp)
116 }
117 rule.awaitIdle()
118
119 rule.onNodeWithTag("scrollbar").performGesture {
120 swipe(start = Offset(10f, 25f), end = Offset(0f, 50f))
121 }
Igor Demin3dfcd042020-12-03 17:08:19 +0300122 rule.awaitIdle()
Igor Demind7332f82020-10-23 21:45:51 +0300123 rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(0.dp)
124 }
125 }
126
127 // TODO(demin): enable after we resolve b/171889442
128 @Ignore("Enable after we resolve b/171889442")
129 @Test
130 fun `mouseScroll over slider`() {
131 runBlocking(Dispatchers.Main) {
132 rule.setContent {
133 TestBox(size = 100.dp, childSize = 20.dp, childCount = 10, scrollbarWidth = 10.dp)
134 }
135 rule.awaitIdle()
136
137 rule.performMouseScroll(0, 25, 1f)
Igor Demin3dfcd042020-12-03 17:08:19 +0300138 rule.awaitIdle()
Igor Demind7332f82020-10-23 21:45:51 +0300139 rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-10.dp)
140 }
141 }
142
143 // TODO(demin): enable after we resolve b/171889442
144 @Ignore("Enable after we resolve b/171889442")
145 @Test
146 fun `mouseScroll over scrollbar outside slider`() {
147 runBlocking(Dispatchers.Main) {
148 rule.setContent {
149 TestBox(size = 100.dp, childSize = 20.dp, childCount = 10, scrollbarWidth = 10.dp)
150 }
151 rule.awaitIdle()
152
153 rule.performMouseScroll(0, 99, 1f)
Igor Demin3dfcd042020-12-03 17:08:19 +0300154 rule.awaitIdle()
Igor Demind7332f82020-10-23 21:45:51 +0300155 rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-10.dp)
156 }
157 }
158
159 // TODO(demin): enable after we resolve b/171889442
160 @Ignore("Enable after we resolve b/171889442")
161 @Test
162 fun `vertical mouseScroll over horizontal scrollbar `() {
163 runBlocking(Dispatchers.Main) {
164 // TODO(demin): write tests for vertical mouse scrolling over
165 // horizontalScrollbar for the case when we have two-way scrollable content:
166 // Modifier.verticalScrollbar(...).horizontalScrollbar(...)
167 // Content should scroll vertically.
168 }
169 }
170
171 @Test
172 fun `mouseScroll over column then drag to the beginning`() {
173 runBlocking(Dispatchers.Main) {
174 rule.setContent {
175 TestBox(size = 100.dp, childSize = 20.dp, childCount = 10, scrollbarWidth = 10.dp)
176 }
177 rule.awaitIdle()
178
179 rule.performMouseScroll(20, 25, 10f)
Igor Demin3dfcd042020-12-03 17:08:19 +0300180 rule.awaitIdle()
Igor Demind7332f82020-10-23 21:45:51 +0300181 rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-100.dp)
182
183 rule.onNodeWithTag("scrollbar").performGesture {
184 swipe(start = Offset(0f, 99f), end = Offset(0f, -500f))
185 }
Igor Demin3dfcd042020-12-03 17:08:19 +0300186 rule.awaitIdle()
Igor Demind7332f82020-10-23 21:45:51 +0300187 rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(0.dp)
188 }
189 }
190
191 @Test(timeout = 3000)
192 fun `press on scrollbar outside slider`() {
193 runBlocking(Dispatchers.Main) {
194 rule.setContent {
195 TestBox(size = 100.dp, childSize = 20.dp, childCount = 20, scrollbarWidth = 10.dp)
196 }
197 rule.awaitIdle()
198
199 rule.onNodeWithTag("scrollbar").performGesture {
200 down(Offset(0f, 26f))
201 }
202
203 tryUntilSucceeded {
Igor Demin3dfcd042020-12-03 17:08:19 +0300204 rule.awaitIdle()
Igor Demind7332f82020-10-23 21:45:51 +0300205 rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-100.dp)
206 }
207 }
208 }
209
210 @Test(timeout = 3000)
211 fun `press on the end of scrollbar outside slider`() {
212 runBlocking(Dispatchers.Main) {
213 rule.setContent {
214 TestBox(size = 100.dp, childSize = 20.dp, childCount = 20, scrollbarWidth = 10.dp)
215 }
216 rule.awaitIdle()
217
218 rule.onNodeWithTag("scrollbar").performGesture {
219 down(Offset(0f, 99f))
220 }
221
222 tryUntilSucceeded {
Igor Demin3dfcd042020-12-03 17:08:19 +0300223 rule.awaitIdle()
Igor Demind7332f82020-10-23 21:45:51 +0300224 rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-300.dp)
225 }
226 }
227 }
228
Igor Demin3bc059e2020-11-25 21:19:35 +0300229 @Test(timeout = 3000)
230 fun `dynamically change content then drag slider to the end`() {
231 runBlocking(Dispatchers.Main) {
232 val isContentVisible = mutableStateOf(false)
233 rule.setContent {
234 TestBox(
235 size = 100.dp,
236 scrollbarWidth = 10.dp
237 ) {
238 if (isContentVisible.value) {
239 repeat(10) {
240 Box(Modifier.size(20.dp).testTag("box$it"))
241 }
242 }
243 }
244 }
Igor Demin3dfcd042020-12-03 17:08:19 +0300245 rule.awaitIdle()
Igor Demin3bc059e2020-11-25 21:19:35 +0300246
247 isContentVisible.value = true
Igor Demin3dfcd042020-12-03 17:08:19 +0300248 rule.awaitIdle()
Igor Demin3bc059e2020-11-25 21:19:35 +0300249
250 rule.onNodeWithTag("scrollbar").performGesture {
251 swipe(start = Offset(0f, 25f), end = Offset(0f, 500f))
252 }
Igor Demin3dfcd042020-12-03 17:08:19 +0300253 rule.awaitIdle()
Igor Demin3bc059e2020-11-25 21:19:35 +0300254 rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-100.dp)
255 }
256 }
257
Igor Demind7332f82020-10-23 21:45:51 +0300258 @Suppress("SameParameterValue")
259 @OptIn(ExperimentalFoundationApi::class)
260 @Test(timeout = 3000)
261 fun `scroll by less than one page in lazy list`() {
262 runBlocking(Dispatchers.Main) {
263 lateinit var state: LazyListState
264
265 rule.setContent {
266 state = rememberLazyListState()
267 LazyTestBox(
268 state,
269 size = 100.dp,
270 childSize = 20.dp,
271 childCount = 20,
272 scrollbarWidth = 10.dp
273 )
274 }
275 rule.awaitIdle()
276
277 rule.onNodeWithTag("scrollbar").performGesture {
George Mount8c41a232021-01-11 21:13:04 +0000278 swipe(start = Offset(0f, 0f), end = Offset(0f, 11f), durationMillis = 1)
Igor Demind7332f82020-10-23 21:45:51 +0300279 }
Igor Demin3dfcd042020-12-03 17:08:19 +0300280 rule.awaitIdle()
Igor Demind7332f82020-10-23 21:45:51 +0300281 assertEquals(2, state.firstVisibleItemIndex)
282 assertEquals(4, state.firstVisibleItemScrollOffset)
283 }
284 }
285
286 @Suppress("SameParameterValue")
287 @OptIn(ExperimentalFoundationApi::class)
288 @Test(timeout = 3000)
289 fun `scroll by more than one page in lazy list`() {
290 runBlocking(Dispatchers.Main) {
291 lateinit var state: LazyListState
292
293 rule.setContent {
294 state = rememberLazyListState()
295 LazyTestBox(
296 state,
297 size = 100.dp,
298 childSize = 20.dp,
299 childCount = 20,
300 scrollbarWidth = 10.dp
301 )
302 }
303 rule.awaitIdle()
304
305 rule.onNodeWithTag("scrollbar").performGesture {
George Mount8c41a232021-01-11 21:13:04 +0000306 swipe(start = Offset(0f, 0f), end = Offset(0f, 26f), durationMillis = 1)
Igor Demind7332f82020-10-23 21:45:51 +0300307 }
Igor Demin3dfcd042020-12-03 17:08:19 +0300308 rule.awaitIdle()
Igor Demind7332f82020-10-23 21:45:51 +0300309 assertEquals(5, state.firstVisibleItemIndex)
310 assertEquals(4, state.firstVisibleItemScrollOffset)
311 }
312 }
313
Igor Demin67e4aa22020-11-19 11:12:43 +0300314 @Suppress("SameParameterValue")
315 @OptIn(ExperimentalFoundationApi::class)
316 @Test(timeout = 3000)
317 fun `scroll outside of scrollbar bounds in lazy list`() {
318 runBlocking(Dispatchers.Main) {
319 lateinit var state: LazyListState
320
321 rule.setContent {
322 state = rememberLazyListState()
323 LazyTestBox(
324 state,
325 size = 100.dp,
326 childSize = 20.dp,
327 childCount = 20,
328 scrollbarWidth = 10.dp
329 )
330 }
331 rule.awaitIdle()
332
333 rule.onNodeWithTag("scrollbar").performGesture {
George Mount8c41a232021-01-11 21:13:04 +0000334 swipe(start = Offset(0f, 0f), end = Offset(0f, 10000f), durationMillis = 1)
Igor Demin67e4aa22020-11-19 11:12:43 +0300335 }
Igor Demin3dfcd042020-12-03 17:08:19 +0300336 rule.awaitIdle()
Igor Demin67e4aa22020-11-19 11:12:43 +0300337 assertEquals(15, state.firstVisibleItemIndex)
338 assertEquals(0, state.firstVisibleItemScrollOffset)
339
340 rule.onNodeWithTag("scrollbar").performGesture {
George Mount8c41a232021-01-11 21:13:04 +0000341 swipe(start = Offset(0f, 99f), end = Offset(0f, -10000f), durationMillis = 1)
Igor Demin67e4aa22020-11-19 11:12:43 +0300342 }
Igor Demin3dfcd042020-12-03 17:08:19 +0300343 rule.awaitIdle()
Igor Demin67e4aa22020-11-19 11:12:43 +0300344 assertEquals(0, state.firstVisibleItemIndex)
345 assertEquals(0, state.firstVisibleItemScrollOffset)
346 }
347 }
348
Igor Demind7332f82020-10-23 21:45:51 +0300349 private suspend fun tryUntilSucceeded(block: suspend () -> Unit) {
350 while (true) {
351 try {
352 block()
353 break
354 } catch (e: Throwable) {
355 delay(10)
356 }
357 }
358 }
359
Igor Demind7332f82020-10-23 21:45:51 +0300360 private fun ComposeTestRule.performMouseScroll(x: Int, y: Int, delta: Float) {
Igor Demind49c0142021-01-22 15:59:54 +0300361 (this as DesktopComposeTestRule).window.onMouseScroll(
Matvei Malkov693e3cc2021-02-05 19:18:12 +0000362 x, y, MouseScrollEvent(MouseScrollUnit.Line(delta), MouseScrollOrientation.Vertical)
Igor Demind7332f82020-10-23 21:45:51 +0300363 )
364 }
365
366 @Composable
367 private fun TestBox(
368 size: Dp,
369 childSize: Dp,
370 childCount: Int,
371 scrollbarWidth: Dp,
372 ) = withTestEnvironment {
373 Box(Modifier.size(size)) {
374 val state = rememberScrollState()
375
Andrey Kulikov05c18b22020-12-14 14:26:44 +0000376 Column(
377 Modifier.fillMaxSize().testTag("column").verticalScroll(state)
Igor Demind7332f82020-10-23 21:45:51 +0300378 ) {
379 repeat(childCount) {
380 Box(Modifier.size(childSize).testTag("box$it"))
381 }
382 }
383
384 VerticalScrollbar(
385 adapter = rememberScrollbarAdapter(state),
386 modifier = Modifier
387 .width(scrollbarWidth)
388 .fillMaxHeight()
389 .testTag("scrollbar")
390 )
391 }
392 }
393
Igor Demin3bc059e2020-11-25 21:19:35 +0300394 @Composable
395 private fun TestBox(
396 size: Dp,
397 scrollbarWidth: Dp,
398 scrollableContent: @Composable ColumnScope.() -> Unit
399 ) = withTestEnvironment {
400 Box(Modifier.size(size)) {
401 val state = rememberScrollState()
402
Andrey Kulikov05c18b22020-12-14 14:26:44 +0000403 Column(
404 Modifier.fillMaxSize().testTag("column").verticalScroll(state),
Igor Demin3bc059e2020-11-25 21:19:35 +0300405 content = scrollableContent
406 )
407
408 VerticalScrollbar(
409 adapter = rememberScrollbarAdapter(state),
410 modifier = Modifier
411 .width(scrollbarWidth)
412 .fillMaxHeight()
413 .testTag("scrollbar")
414 )
415 }
416 }
417
Igor Demind7332f82020-10-23 21:45:51 +0300418 @Suppress("SameParameterValue")
419 @OptIn(ExperimentalFoundationApi::class)
420 @Composable
421 private fun LazyTestBox(
422 state: LazyListState,
423 size: Dp,
424 childSize: Dp,
425 childCount: Int,
426 scrollbarWidth: Dp,
427 ) = withTestEnvironment {
428 Box(Modifier.size(size)) {
Andrey Kulikov1e8ebd32020-12-08 22:12:16 +0000429 LazyColumn(
Igor Demind7332f82020-10-23 21:45:51 +0300430 Modifier.fillMaxSize().testTag("column"),
431 state
432 ) {
Andrey Kulikov1e8ebd32020-12-08 22:12:16 +0000433 items((0 until childCount).toList()) {
434 Box(Modifier.size(childSize).testTag("box$it"))
435 }
Igor Demind7332f82020-10-23 21:45:51 +0300436 }
437
438 VerticalScrollbar(
439 adapter = rememberScrollbarAdapter(state, childCount, childSize),
440 modifier = Modifier
441 .width(scrollbarWidth)
442 .fillMaxHeight()
443 .testTag("scrollbar")
444 )
445 }
446 }
447
448 @Composable
Leland Richardson0f99bf12021-02-02 20:56:41 -0800449 private fun withTestEnvironment(content: @Composable () -> Unit) = CompositionLocalProvider(
Igor Demind7332f82020-10-23 21:45:51 +0300450 ScrollbarStyleAmbient provides ScrollbarStyle(
451 minimalHeight = 16.dp,
452 thickness = 8.dp,
453 shape = RectangleShape,
454 hoverDurationMillis = 300,
455 unhoverColor = Color.Black,
456 hoverColor = Color.Red
457 ),
458 DesktopPlatformAmbient provides DesktopPlatform.MacOS,
Igor Demind3e0f022020-11-18 13:20:30 +0300459 content = content
Igor Demind7332f82020-10-23 21:45:51 +0300460 )
Jelle Fresen8e55c4f2020-12-16 13:15:53 +0000461}