Merge "Make nested scrolling tests depend less on implementation" into main
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index def8323..5a9edba 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -137,13 +137,6 @@
 
         var pointerInfoOwner: () -> PointersInfo = { pointersDown() }
 
-        fun nestedScrollConnection() =
-            NestedScrollHandlerImpl(
-                    draggableHandler = draggableHandler,
-                    pointersInfoOwner = { pointerInfoOwner() },
-                )
-                .connection
-
         val velocityThreshold = draggableHandler.velocityThreshold
 
         fun down(fractionOfScreen: Float) =
@@ -607,57 +600,6 @@
     }
 
     @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.
         val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
@@ -689,24 +631,6 @@
     }
 
     @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.
         layoutState.transitions = transitions {
@@ -944,33 +868,4 @@
         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)
-    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index e80805a..0355a30 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -40,6 +40,7 @@
 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.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 @@
         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)
+    }
 }