Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4a5b3c4d authored by Jordan Demeulenaere's avatar Jordan Demeulenaere Committed by Android (Google) Code Review
Browse files

Merge "Make nested scrolling tests depend less on implementation" into main

parents 40d62a22 0f16c025
Loading
Loading
Loading
Loading
+0 −105
Original line number Diff line number Diff line
@@ -137,13 +137,6 @@ class DraggableHandlerTest {

        var pointerInfoOwner: () -> PointersInfo = { pointersDown() }

        fun nestedScrollConnection() =
            NestedScrollHandlerImpl(
                    draggableHandler = draggableHandler,
                    pointersInfoOwner = { pointerInfoOwner() },
                )
                .connection

        val velocityThreshold = draggableHandler.velocityThreshold

        fun down(fractionOfScreen: Float) =
@@ -606,57 +599,6 @@ class DraggableHandlerTest {
        assertIdle(SceneA)
    }

    @Test
    fun nestedScrollUseFromSourceInfo() = runGestureTest {
        // Start at scene C.
        navigateToSceneC()
        val nestedScroll = nestedScrollConnection()

        // Drag from the **top** of the screen
        pointerInfoOwner = { pointersDown() }
        assertIdle(currentScene = SceneC)

        nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
        assertTransition(
            currentScene = SceneC,
            fromScene = SceneC,
            // userAction: Swipe.Up to SceneB
            toScene = SceneB,
            progress = 0.1f,
        )

        // Reset to SceneC
        nestedScroll.preFling(Velocity.Zero)
        advanceUntilIdle()

        // Drag from the **bottom** of the screen
        pointerInfoOwner = { pointersDown(startedPosition = Offset(0f, SCREEN_SIZE)) }
        assertIdle(currentScene = SceneC)

        nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
        assertTransition(
            currentScene = SceneC,
            fromScene = SceneC,
            // userAction: Swipe.Up(fromSource = Edge.Bottom) to SceneA
            toScene = SceneA,
            progress = 0.1f,
        )
    }

    @Test
    fun ignoreMouseWheel() = runGestureTest {
        // Start at scene C.
        navigateToSceneC()
        val nestedScroll = nestedScrollConnection()

        // Use mouse wheel
        pointerInfoOwner = { PointersInfo.MouseWheel }
        assertIdle(currentScene = SceneC)

        nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
        assertIdle(currentScene = SceneC)
    }

    @Test
    fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest {
        // Swipe up from the middle to transition to scene B.
@@ -688,24 +630,6 @@ class DraggableHandlerTest {
        )
    }

    @Test
    fun scrollKeepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() = runGestureTest {
        val nestedScroll = nestedScrollConnection()

        // Overscroll is disabled, it will scroll up to 100%
        nestedScroll.scroll(available = upOffset(fractionOfScreen = 2f))
        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)

        // We need to maintain scroll priority even if the scene transition can no longer consume
        // the scroll gesture.
        nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)

        // A scroll gesture in the opposite direction allows us to return to the previous scene.
        nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.5f))
        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.5f)
    }

    @Test
    fun overscroll_releaseBetween0And100Percent_up() = runGestureTest {
        // Make scene B overscrollable.
@@ -944,33 +868,4 @@ class DraggableHandlerTest {
        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
        assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB)
    }

    @Test
    fun replaceOverlayNestedScroll() = runGestureTest {
        layoutState.showOverlay(OverlayA, animationScope = testScope)
        advanceUntilIdle()

        // Initial state.
        assertThat(layoutState.transitionState).isIdle()
        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
        assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayA)

        // Swipe down to replace overlay A by overlay B.

        val nestedScroll = nestedScrollConnection()
        nestedScroll.scroll(downOffset(0.1f))
        val transition = assertThat(layoutState.transitionState).isReplaceOverlayTransition()
        assertThat(transition).hasCurrentScene(SceneA)
        assertThat(transition).hasFromOverlay(OverlayA)
        assertThat(transition).hasToOverlay(OverlayB)
        assertThat(transition).hasCurrentOverlays(OverlayA)
        assertThat(transition).hasProgress(0.1f)

        nestedScroll.preFling(Velocity(0f, velocityThreshold))
        advanceUntilIdle()
        // Commit the gesture. The overlays are instantly swapped in the set of current overlays.
        assertThat(layoutState.transitionState).isIdle()
        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
        assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB)
    }
}
+163 −0
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ScrollWheel
import androidx.compose.ui.test.TouchInjectionScope
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule
@@ -55,6 +56,8 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.TestOverlays.OverlayA
import com.android.compose.animation.scene.TestOverlays.OverlayB
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
@@ -977,4 +980,164 @@ class SwipeToSceneTest {
        rule.waitForIdle()
        assertThat(state.transitionState).isSceneTransition()
    }

    @Test
    fun nestedScroll_useFromSourceInfo() {
        val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
        var touchSlop = 0f
        rule.setContent {
            touchSlop = LocalViewConfiguration.current.touchSlop
            SceneTransitionLayout(state) {
                scene(
                    SceneA,
                    userActions =
                        mapOf(Swipe.Down to SceneB, Swipe.Down(fromSource = Edge.Top) to SceneC),
                ) {
                    // Use a fullscreen nested scrollable to use the nested scroll connection.
                    Box(
                        Modifier.fillMaxSize()
                            .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
                    )
                }
                scene(SceneB) { Box(Modifier.fillMaxSize()) }
                scene(SceneC) { Box(Modifier.fillMaxSize()) }
            }
        }

        // Swiping down from the middle of the screen leads to B.
        rule.onRoot().performTouchInput {
            down(center)
            moveBy(Offset(0f, touchSlop + 1f))
        }

        var transition = assertThat(state.transitionState).isSceneTransition()
        assertThat(transition).hasFromScene(SceneA)
        assertThat(transition).hasToScene(SceneB)

        // Release finger and wait to settle back to A.
        rule.onRoot().performTouchInput { up() }
        rule.waitForIdle()
        assertThat(state.transitionState).isIdle()
        assertThat(state.transitionState).hasCurrentScene(SceneA)

        // Swiping down from the top of the screen leads to B.
        rule.onRoot().performTouchInput {
            down(center.copy(y = 0f))
            moveBy(Offset(0f, touchSlop + 1f))
        }

        transition = assertThat(state.transitionState).isSceneTransition()
        assertThat(transition).hasFromScene(SceneA)
        assertThat(transition).hasToScene(SceneC)
    }

    @Test
    fun nestedScroll_ignoreMouseWheel() {
        val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
        var touchSlop = 0f
        rule.setContent {
            touchSlop = LocalViewConfiguration.current.touchSlop
            SceneTransitionLayout(state) {
                scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
                    // Use a fullscreen nested scrollable to use the nested scroll connection.
                    Box(
                        Modifier.fillMaxSize()
                            .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
                    )
                }
                scene(SceneB) { Box(Modifier.fillMaxSize()) }
            }
        }

        rule.onRoot().performMouseInput {
            scroll(-touchSlop - 1f, scrollWheel = ScrollWheel.Vertical)
        }
        assertThat(state.transitionState).isIdle()
    }

    @Test
    fun nestedScroll_keepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() {
        val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
        var touchSlop = 0f
        rule.setContent {
            touchSlop = LocalViewConfiguration.current.touchSlop
            SceneTransitionLayout(state) {
                scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
                    // Use a fullscreen nested scrollable to use the nested scroll connection.
                    Box(
                        Modifier.fillMaxSize()
                            .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
                    )
                }
                scene(SceneB) { Box(Modifier.fillMaxSize()) }
            }
        }

        fun TouchInjectionScope.height() = bottom
        fun TouchInjectionScope.halfHeight() = height() / 2f

        rule.onRoot().performTouchInput {
            down(center.copy(y = 0f))
            moveBy(Offset(0f, touchSlop + halfHeight()))
        }
        val transition = assertThat(state.transitionState).isSceneTransition()
        assertThat(transition).hasProgress(0.5f, tolerance = 0.01f)

        // The progress should never go above 100%.
        rule.onRoot().performTouchInput { moveBy(Offset(0f, height())) }
        assertThat(transition).hasProgress(1f, tolerance = 0.01f)

        // Because the overscroll effect of scene B is not attached, swiping in the opposite
        // direction will directly decrease the progress.
        rule.onRoot().performTouchInput { moveBy(Offset(0f, -halfHeight())) }
        assertThat(transition).hasProgress(0.5f, tolerance = 0.01f)
    }

    @Test
    fun nestedScroll_replaceOverlay() {
        val state =
            rule.runOnUiThread {
                MutableSceneTransitionLayoutState(SceneA, initialOverlays = setOf(OverlayA))
            }
        var touchSlop = 0f
        rule.setContent {
            touchSlop = LocalViewConfiguration.current.touchSlop
            SceneTransitionLayout(state) {
                scene(SceneA) { Box(Modifier.fillMaxSize()) }
                overlay(
                    OverlayA,
                    mapOf(Swipe.Down to UserActionResult.ReplaceByOverlay(OverlayB)),
                ) {
                    Box(
                        Modifier.fillMaxSize()
                            .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
                    )
                }
                overlay(OverlayB) { Box(Modifier.fillMaxSize()) }
            }
        }

        // Swipe down 100% to replace A by B.
        rule.onRoot().performTouchInput {
            down(center.copy(y = 0f))
            moveBy(Offset(0f, touchSlop + bottom))
        }

        val transition = assertThat(state.transitionState).isReplaceOverlayTransition()
        assertThat(transition).hasCurrentScene(SceneA)
        assertThat(transition).hasFromOverlay(OverlayA)
        assertThat(transition).hasToOverlay(OverlayB)
        assertThat(transition).hasCurrentOverlays(OverlayA)
        assertThat(transition).hasProgress(1f, tolerance = 0.01f)

        // Commit the gesture. The overlays are instantly swapped in the set of current overlays.
        rule.onRoot().performTouchInput { up() }
        assertThat(transition).hasCurrentScene(SceneA)
        assertThat(transition).hasCurrentOverlays(OverlayB)

        rule.waitForIdle()
        assertThat(state.transitionState).isIdle()
        assertThat(state.transitionState).hasCurrentScene(SceneA)
        assertThat(state.transitionState).hasCurrentOverlays(OverlayB)
    }
}