1 /*
2  * Copyright 2022 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 
17 package androidx.compose.ui.viewinterop
18 
19 import android.os.Build
20 import androidx.compose.foundation.gestures.Orientation
21 import androidx.compose.foundation.gestures.rememberScrollableState
22 import androidx.compose.foundation.gestures.scrollable
23 import androidx.compose.foundation.layout.Box
24 import androidx.compose.ui.Modifier
25 import androidx.compose.ui.geometry.Offset
26 import androidx.compose.ui.input.nestedscroll.nestedScroll
27 import androidx.compose.ui.test.junit4.createComposeRule
28 import androidx.compose.ui.tests.R
29 import androidx.compose.ui.unit.Velocity
30 import androidx.test.espresso.Espresso
31 import androidx.test.espresso.Espresso.onView
32 import androidx.test.espresso.action.ViewActions.swipeUp
33 import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition
34 import androidx.test.espresso.matcher.ViewMatchers.withId
35 import androidx.test.ext.junit.runners.AndroidJUnit4
36 import androidx.test.filters.MediumTest
37 import androidx.test.filters.SdkSuppress
38 import com.google.common.truth.Truth.assertThat
39 import org.junit.Before
40 import org.junit.Rule
41 import org.junit.Test
42 import org.junit.runner.RunWith
43 
44 @MediumTest
45 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
46 @RunWith(AndroidJUnit4::class)
47 class NestedScrollInteropViewHolderTest {
48     @get:Rule val rule = createComposeRule()
49 
50     private val connection = InspectableNestedScrollConnection()
51     private val recyclerViewConsumptionTracker = RecyclerViewConsumptionTracker()
52 
53     @Before
setUpnull54     fun setUp() {
55         connection.reset()
56         recyclerViewConsumptionTracker.reset()
57     }
58 
59     @Test
nestedScrollInteropIsOff_shouldNotPropagateDeltasnull60     fun nestedScrollInteropIsOff_shouldNotPropagateDeltas() {
61         // arrange
62         rule.setContent {
63             NestedScrollInteropWithView(
64                 modifier = Modifier.nestedScroll(connection),
65                 enabled = false,
66                 recyclerViewConsumptionTracker = recyclerViewConsumptionTracker
67             )
68         }
69 
70         // act
71         onView(withId(R.id.main_list))
72             .perform(scrollToPosition<NestedScrollInteropAdapter.SimpleTextViewHolder>(20))
73 
74         // assert
75         rule.runOnIdle { assertThat(connection.offeredFromChild).isEqualTo(Offset.Zero) }
76     }
77 
78     @Test
nestedScrollInteropIsOn_shouldPropagateDeltasnull79     fun nestedScrollInteropIsOn_shouldPropagateDeltas() {
80         // arrange
81         rule.setContent {
82             NestedScrollInteropWithView(
83                 modifier = Modifier.nestedScroll(connection),
84                 enabled = true,
85                 recyclerViewConsumptionTracker = recyclerViewConsumptionTracker
86             )
87         }
88 
89         // act
90         onView(withId(R.id.main_layout)).perform(swipeUp())
91 
92         rule.waitForIdle()
93 
94         // assert
95         rule.runOnIdle { assertThat(connection.offeredFromChild).isNotEqualTo(Offset.Zero) }
96     }
97 
98     @Test
nestedScrollInteropIsOn_checkDeltasCorrectlyPropagatenull99     fun nestedScrollInteropIsOn_checkDeltasCorrectlyPropagate() {
100         // arrange
101         rule.setContent {
102             NestedScrollInteropWithView(
103                 modifier = Modifier.nestedScroll(connection),
104                 enabled = true,
105                 recyclerViewConsumptionTracker = recyclerViewConsumptionTracker
106             )
107         }
108 
109         // act
110         onView(withId(R.id.main_list)).perform(swipeUp())
111 
112         // assert
113         rule.runOnIdle {
114             assertThat(connection.offeredFromChild)
115                 .isEqualTo(recyclerViewConsumptionTracker.deltaConsumed)
116         }
117     }
118 
119     @Test
nestedScrollInteropIsOn_checkDeltasCorrectlyPropagatePostScrollnull120     fun nestedScrollInteropIsOn_checkDeltasCorrectlyPropagatePostScroll() {
121         // arrange
122         rule.setContent {
123             NestedScrollInteropWithView(
124                 modifier = Modifier.nestedScroll(connection),
125                 enabled = true,
126                 recyclerViewConsumptionTracker = recyclerViewConsumptionTracker
127             )
128         }
129 
130         // act
131         onView(withId(R.id.main_list)).perform(swipeUp())
132 
133         // assert
134         rule.runOnIdle {
135             assertThat(connection.notConsumedByChild).isEqualTo(Offset.Zero)
136             assertThat(connection.consumedDownChain)
137                 .isEqualTo(recyclerViewConsumptionTracker.deltaConsumed)
138         }
139     }
140 
141     @Test
nestedScrollInteropIsOn_consumedUpChain_checkDeltasCorrectlyPropagatePostScrollnull142     fun nestedScrollInteropIsOn_consumedUpChain_checkDeltasCorrectlyPropagatePostScroll() {
143         // arrange
144         rule.setContent {
145             val controller = rememberScrollableState { it }
146 
147             Box(modifier = Modifier.scrollable(controller, Orientation.Vertical)) {
148                 NestedScrollInteropWithView(
149                     modifier = Modifier.nestedScroll(connection),
150                     enabled = true,
151                     recyclerViewConsumptionTracker = recyclerViewConsumptionTracker
152                 )
153             }
154         }
155 
156         // act
157         Espresso.onView(withId(R.id.main_list)).perform(swipeUp())
158 
159         // assert
160         rule.runOnIdle {
161             assertThat(recyclerViewConsumptionTracker.deltaConsumed).isEqualTo(Offset.Zero)
162             assertThat(connection.notConsumedByChild).isEqualTo(Offset.Zero)
163         }
164     }
165 
166     @Test
nestedScrollInteropIsOn_checkDeltasCorrectlyPropagatePreFlingnull167     fun nestedScrollInteropIsOn_checkDeltasCorrectlyPropagatePreFling() {
168         // arrange
169         rule.setContent {
170             NestedScrollInteropWithView(
171                 modifier = Modifier.nestedScroll(connection),
172                 enabled = true,
173                 recyclerViewConsumptionTracker = recyclerViewConsumptionTracker
174             )
175         }
176 
177         // act
178         onView(withId(R.id.main_list)).perform(swipeUp())
179         rule.waitForIdle()
180         // assert
181         rule.runOnIdle {
182             assertThat(abs(connection.velocityOfferedFromChild))
183                 .isEqualTo(abs(recyclerViewConsumptionTracker.velocityConsumed))
184         }
185     }
186 
187     @Test
nestedScrollInteropIsOn_checkDeltasCorrectlyPropagatePostFlingnull188     fun nestedScrollInteropIsOn_checkDeltasCorrectlyPropagatePostFling() {
189         // arrange
190         rule.setContent {
191             NestedScrollInteropWithView(
192                 modifier = Modifier.nestedScroll(connection),
193                 enabled = true,
194                 recyclerViewConsumptionTracker = recyclerViewConsumptionTracker
195             )
196         }
197 
198         // act
199         onView(withId(R.id.main_list)).perform(swipeUp())
200 
201         // assert
202         rule.runOnIdle {
203             assertThat(connection.velocityNotConsumedByChild).isEqualTo(Velocity.Zero)
204             assertThat(connection.velocityConsumedDownChain)
205                 .isEqualTo(recyclerViewConsumptionTracker.velocityConsumed)
206         }
207     }
208 }
209