diff options
| author | 2025-01-09 13:39:15 +0100 | |
|---|---|---|
| committer | 2025-01-09 15:05:10 +0100 | |
| commit | 0f16c025183924a529647d8732b4deb75a964ffb (patch) | |
| tree | d47e0b18286edf60dbc0a0822d90137de14d23dd | |
| parent | acc993fd2b0e3a073cd7b176a20fa59e0ca798b7 (diff) | |
Make nested scrolling tests depend less on implementation
This CL migrates the nested scroll tests in DraggableHandler to higher
level tests that use the Compose testing API. This will allow to replace
the implementation of the nested scroll implementation witout having to
change the tests, and is the last step towards submitting ag/30549444.
Bug: 378470603
Test: atest SwipeToSceneTest
Flag: EXEMPT test refactor
Change-Id: I09a9ffb7fad410cef246b167e6b91560f79ea9fb
2 files changed, 163 insertions, 105 deletions
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 dbac62ffb713..9c1a14dec8fa 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 @@ class DraggableHandlerTest { var pointerInfoOwner: () -> PointersInfo = { pointersDown() } - fun nestedScrollConnection() = - NestedScrollHandlerImpl( - draggableHandler = draggableHandler, - pointersInfoOwner = { pointerInfoOwner() }, - ) - .connection - val velocityThreshold = draggableHandler.velocityThreshold fun down(fractionOfScreen: Float) = @@ -607,57 +600,6 @@ class DraggableHandlerTest { } @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 @@ 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. layoutState.transitions = transitions { @@ -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) - } } 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 e80805a4e374..0355a30d5c73 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.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) + } } |