1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.android_webview.robolectric;
6 
7 import android.graphics.Rect;
8 
9 import androidx.test.filters.SmallTest;
10 
11 import org.junit.Assert;
12 import org.junit.Test;
13 import org.junit.runner.RunWith;
14 import org.robolectric.annotation.Config;
15 
16 import org.chromium.android_webview.AwScrollOffsetManager;
17 import org.chromium.base.test.util.Feature;
18 import org.chromium.testing.local.LocalRobolectricTestRunner;
19 
20 /**
21  * Integration tests for ScrollOffsetManager.
22  */
23 @RunWith(LocalRobolectricTestRunner.class)
24 @Config(manifest = Config.NONE)
25 public class AwScrollOffsetManagerTest {
26     private static class TestScrollOffsetManagerDelegate implements AwScrollOffsetManager.Delegate {
27         private int mOverScrollDeltaX;
28         private int mOverScrollDeltaY;
29         private int mOverScrollCallCount;
30         private int mScrollX;
31         private int mScrollY;
32         private int mNativeScrollX;
33         private int mNativeScrollY;
34         private int mInvalidateCount;
35 
getOverScrollDeltaX()36         public int getOverScrollDeltaX() {
37             return mOverScrollDeltaX;
38         }
39 
getOverScrollDeltaY()40         public int getOverScrollDeltaY() {
41             return mOverScrollDeltaY;
42         }
43 
getOverScrollCallCount()44         public int getOverScrollCallCount() {
45             return mOverScrollCallCount;
46         }
47 
getScrollX()48         public int getScrollX() {
49             return mScrollX;
50         }
51 
getScrollY()52         public int getScrollY() {
53             return mScrollY;
54         }
55 
getNativeScrollX()56         public int getNativeScrollX() {
57             return mNativeScrollX;
58         }
59 
getNativeScrollY()60         public int getNativeScrollY() {
61             return mNativeScrollY;
62         }
63 
getInvalidateCount()64         public int getInvalidateCount() {
65             return mInvalidateCount;
66         }
67 
68         @Override
overScrollContainerViewBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, boolean isTouchEvent)69         public void overScrollContainerViewBy(int deltaX, int deltaY, int scrollX, int scrollY,
70                 int scrollRangeX, int scrollRangeY, boolean isTouchEvent) {
71             mOverScrollDeltaX = deltaX;
72             mOverScrollDeltaY = deltaY;
73             mOverScrollCallCount += 1;
74         }
75 
76         @Override
scrollContainerViewTo(int x, int y)77         public void scrollContainerViewTo(int x, int y) {
78             mScrollX = x;
79             mScrollY = y;
80         }
81 
82         @Override
scrollNativeTo(int x, int y)83         public void scrollNativeTo(int x, int y) {
84             mNativeScrollX = x;
85             mNativeScrollY = y;
86         }
87 
88         @Override
getContainerViewScrollX()89         public int getContainerViewScrollX() {
90             return mScrollX;
91         }
92 
93         @Override
getContainerViewScrollY()94         public int getContainerViewScrollY() {
95             return mScrollY;
96         }
97 
98         @Override
invalidate()99         public void invalidate() {
100             mInvalidateCount += 1;
101         }
102 
103         @Override
cancelFling()104         public void cancelFling() {}
105 
106         @Override
smoothScroll(int targetX, int targetY, long durationMs)107         public void smoothScroll(int targetX, int targetY, long durationMs) {}
108     }
109 
simulateScrolling(AwScrollOffsetManager offsetManager, TestScrollOffsetManagerDelegate delegate, int scrollX, int scrollY)110     private void simulateScrolling(AwScrollOffsetManager offsetManager,
111             TestScrollOffsetManagerDelegate delegate, int scrollX, int scrollY) {
112         // Scrolling is a two-phase action. First we ask the manager to scroll
113         int callCount = delegate.getOverScrollCallCount();
114         offsetManager.scrollContainerViewTo(scrollX, scrollY);
115         // The manager then asks the delegate to overscroll the view.
116         Assert.assertEquals(callCount + 1, delegate.getOverScrollCallCount());
117         Assert.assertEquals(scrollX, delegate.getOverScrollDeltaX() + delegate.getScrollX());
118         Assert.assertEquals(scrollY, delegate.getOverScrollDeltaY() + delegate.getScrollY());
119         // As a response to that the menager expects the view to call back with the new scroll.
120         offsetManager.onContainerViewOverScrolled(scrollX, scrollY, false, false);
121     }
122 
simlateOverScrollPropagation( AwScrollOffsetManager offsetManager, TestScrollOffsetManagerDelegate delegate)123     private void simlateOverScrollPropagation(
124             AwScrollOffsetManager offsetManager, TestScrollOffsetManagerDelegate delegate) {
125         Assert.assertTrue(delegate.getOverScrollCallCount() > 0);
126 
127         offsetManager.onContainerViewOverScrolled(
128                 delegate.getOverScrollDeltaX() + delegate.getScrollX(),
129                 delegate.getOverScrollDeltaY() + delegate.getScrollY(), false, false);
130     }
131 
132     @Test
133     @SmallTest
134     @Feature({"AndroidWebView"})
testWhenContentSizeMatchesView()135     public void testWhenContentSizeMatchesView() {
136         TestScrollOffsetManagerDelegate delegate = new TestScrollOffsetManagerDelegate();
137         AwScrollOffsetManager offsetManager = new AwScrollOffsetManager(delegate);
138 
139         final int width = 132;
140         final int height = 212;
141         final int scrollX = 11;
142         final int scrollY = 13;
143 
144         offsetManager.setMaxScrollOffset(0, 0);
145         offsetManager.setContainerViewSize(width, height);
146 
147         Assert.assertEquals(width, offsetManager.computeHorizontalScrollRange());
148         Assert.assertEquals(height, offsetManager.computeVerticalScrollRange());
149 
150         // Since the view size and contents size are equal no scrolling should be possible.
151         Assert.assertEquals(0, offsetManager.computeMaximumHorizontalScrollOffset());
152         Assert.assertEquals(0, offsetManager.computeMaximumVerticalScrollOffset());
153 
154         // Scrolling should generate overscroll but not update the scroll offset.
155         simulateScrolling(offsetManager, delegate, scrollX, scrollY);
156         Assert.assertEquals(scrollX, delegate.getOverScrollDeltaX());
157         Assert.assertEquals(scrollY, delegate.getOverScrollDeltaY());
158         Assert.assertEquals(0, delegate.getScrollX());
159         Assert.assertEquals(0, delegate.getScrollY());
160         Assert.assertEquals(0, delegate.getNativeScrollX());
161         Assert.assertEquals(0, delegate.getNativeScrollY());
162 
163         // Scrolling to 0,0 should result in no deltas.
164         simulateScrolling(offsetManager, delegate, 0, 0);
165         Assert.assertEquals(0, delegate.getOverScrollDeltaX());
166         Assert.assertEquals(0, delegate.getOverScrollDeltaY());
167 
168         // Negative scrolling should result in negative deltas but no scroll offset update.
169         simulateScrolling(offsetManager, delegate, -scrollX, -scrollY);
170         Assert.assertEquals(-scrollX, delegate.getOverScrollDeltaX());
171         Assert.assertEquals(-scrollY, delegate.getOverScrollDeltaY());
172         Assert.assertEquals(0, delegate.getScrollX());
173         Assert.assertEquals(0, delegate.getScrollY());
174         Assert.assertEquals(0, delegate.getNativeScrollX());
175         Assert.assertEquals(0, delegate.getNativeScrollY());
176     }
177 
178     private static final int VIEW_WIDTH = 211;
179     private static final int VIEW_HEIGHT = 312;
180     private static final int MAX_HORIZONTAL_OFFSET = 757;
181     private static final int MAX_VERTICAL_OFFSET = 127;
182     private static final int CONTENT_WIDTH = VIEW_WIDTH + MAX_HORIZONTAL_OFFSET;
183     private static final int CONTENT_HEIGHT = VIEW_HEIGHT + MAX_VERTICAL_OFFSET;
184 
185     @Test
186     @SmallTest
187     @Feature({"AndroidWebView"})
testScrollRangeAndMaxOffset()188     public void testScrollRangeAndMaxOffset() {
189         TestScrollOffsetManagerDelegate delegate = new TestScrollOffsetManagerDelegate();
190         AwScrollOffsetManager offsetManager = new AwScrollOffsetManager(delegate);
191 
192         offsetManager.setMaxScrollOffset(MAX_HORIZONTAL_OFFSET, MAX_VERTICAL_OFFSET);
193         offsetManager.setContainerViewSize(VIEW_WIDTH, VIEW_HEIGHT);
194 
195         Assert.assertEquals(CONTENT_WIDTH, offsetManager.computeHorizontalScrollRange());
196         Assert.assertEquals(CONTENT_HEIGHT, offsetManager.computeVerticalScrollRange());
197 
198         Assert.assertEquals(
199                 MAX_HORIZONTAL_OFFSET, offsetManager.computeMaximumHorizontalScrollOffset());
200         Assert.assertEquals(
201                 MAX_VERTICAL_OFFSET, offsetManager.computeMaximumVerticalScrollOffset());
202 
203         // Scrolling beyond the maximum should be clamped.
204         final int scrollX = MAX_HORIZONTAL_OFFSET + 10;
205         final int scrollY = MAX_VERTICAL_OFFSET + 11;
206 
207         simulateScrolling(offsetManager, delegate, scrollX, scrollY);
208         Assert.assertEquals(scrollX, delegate.getOverScrollDeltaX());
209         Assert.assertEquals(scrollY, delegate.getOverScrollDeltaY());
210         Assert.assertEquals(MAX_HORIZONTAL_OFFSET, delegate.getScrollX());
211         Assert.assertEquals(MAX_VERTICAL_OFFSET, delegate.getScrollY());
212         Assert.assertEquals(MAX_HORIZONTAL_OFFSET, delegate.getNativeScrollX());
213         Assert.assertEquals(MAX_VERTICAL_OFFSET, delegate.getNativeScrollY());
214 
215         // Scrolling to negative coordinates should be clamped back to 0,0.
216         simulateScrolling(offsetManager, delegate, -scrollX, -scrollY);
217         Assert.assertEquals(0, delegate.getScrollX());
218         Assert.assertEquals(0, delegate.getScrollY());
219         Assert.assertEquals(0, delegate.getNativeScrollX());
220         Assert.assertEquals(0, delegate.getNativeScrollY());
221 
222         // The onScrollChanged method is callable by third party code and should also be clamped
223         offsetManager.onContainerViewScrollChanged(scrollX, scrollY);
224         Assert.assertEquals(MAX_HORIZONTAL_OFFSET, delegate.getNativeScrollX());
225         Assert.assertEquals(MAX_VERTICAL_OFFSET, delegate.getNativeScrollY());
226 
227         offsetManager.onContainerViewScrollChanged(-scrollX, -scrollY);
228         Assert.assertEquals(0, delegate.getNativeScrollX());
229         Assert.assertEquals(0, delegate.getNativeScrollY());
230     }
231 
232     @Test
233     @SmallTest
234     @Feature({"AndroidWebView"})
testDelegateCanOverrideScroll()235     public void testDelegateCanOverrideScroll() {
236         final int overrideScrollX = 10;
237         final int overrideScrollY = 10;
238 
239         TestScrollOffsetManagerDelegate delegate = new TestScrollOffsetManagerDelegate() {
240             @Override
241             public int getContainerViewScrollX() {
242                 return overrideScrollX;
243             }
244 
245             @Override
246             public int getContainerViewScrollY() {
247                 return overrideScrollY;
248             }
249         };
250         AwScrollOffsetManager offsetManager = new AwScrollOffsetManager(delegate);
251 
252         offsetManager.setMaxScrollOffset(MAX_HORIZONTAL_OFFSET, MAX_VERTICAL_OFFSET);
253         offsetManager.setContainerViewSize(VIEW_WIDTH, VIEW_HEIGHT);
254 
255         offsetManager.onContainerViewOverScrolled(0, 0, false, false);
256         Assert.assertEquals(overrideScrollX, delegate.getNativeScrollX());
257         Assert.assertEquals(overrideScrollY, delegate.getNativeScrollY());
258     }
259 
260     @Test
261     @SmallTest
262     @Feature({"AndroidWebView"})
testDelegateOverridenScrollsDontExceedBounds()263     public void testDelegateOverridenScrollsDontExceedBounds() {
264         final int overrideScrollX = MAX_HORIZONTAL_OFFSET + 10;
265         final int overrideScrollY = MAX_VERTICAL_OFFSET + 20;
266         TestScrollOffsetManagerDelegate delegate = new TestScrollOffsetManagerDelegate() {
267             @Override
268             public int getContainerViewScrollX() {
269                 return overrideScrollX;
270             }
271 
272             @Override
273             public int getContainerViewScrollY() {
274                 return overrideScrollY;
275             }
276         };
277         AwScrollOffsetManager offsetManager = new AwScrollOffsetManager(delegate);
278 
279         offsetManager.setMaxScrollOffset(MAX_HORIZONTAL_OFFSET, MAX_VERTICAL_OFFSET);
280         offsetManager.setContainerViewSize(VIEW_WIDTH, VIEW_HEIGHT);
281 
282         offsetManager.onContainerViewOverScrolled(0, 0, false, false);
283         Assert.assertEquals(MAX_HORIZONTAL_OFFSET, delegate.getNativeScrollX());
284         Assert.assertEquals(MAX_VERTICAL_OFFSET, delegate.getNativeScrollY());
285     }
286 
287     @Test
288     @SmallTest
289     @Feature({"AndroidWebView"})
testScrollContainerViewTo()290     public void testScrollContainerViewTo() {
291         TestScrollOffsetManagerDelegate delegate = new TestScrollOffsetManagerDelegate();
292         AwScrollOffsetManager offsetManager = new AwScrollOffsetManager(delegate);
293 
294         final int scrollX = 31;
295         final int scrollY = 41;
296 
297         offsetManager.setMaxScrollOffset(MAX_HORIZONTAL_OFFSET, MAX_VERTICAL_OFFSET);
298         offsetManager.setContainerViewSize(VIEW_WIDTH, VIEW_HEIGHT);
299 
300         Assert.assertEquals(0, delegate.getOverScrollDeltaX());
301         Assert.assertEquals(0, delegate.getOverScrollDeltaY());
302         int callCount = delegate.getOverScrollCallCount();
303 
304         offsetManager.scrollContainerViewTo(scrollX, scrollY);
305         Assert.assertEquals(callCount + 1, delegate.getOverScrollCallCount());
306         Assert.assertEquals(scrollX, delegate.getOverScrollDeltaX());
307         Assert.assertEquals(scrollY, delegate.getOverScrollDeltaY());
308     }
309 
310     @Test
311     @SmallTest
312     @Feature({"AndroidWebView"})
testOnContainerViewOverScrolled()313     public void testOnContainerViewOverScrolled() {
314         TestScrollOffsetManagerDelegate delegate = new TestScrollOffsetManagerDelegate();
315         AwScrollOffsetManager offsetManager = new AwScrollOffsetManager(delegate);
316 
317         final int scrollX = 31;
318         final int scrollY = 41;
319 
320         offsetManager.setMaxScrollOffset(MAX_HORIZONTAL_OFFSET, MAX_VERTICAL_OFFSET);
321         offsetManager.setContainerViewSize(VIEW_WIDTH, VIEW_HEIGHT);
322 
323         Assert.assertEquals(0, delegate.getScrollX());
324         Assert.assertEquals(0, delegate.getScrollY());
325         Assert.assertEquals(0, delegate.getNativeScrollX());
326         Assert.assertEquals(0, delegate.getNativeScrollY());
327 
328         offsetManager.onContainerViewOverScrolled(scrollX, scrollY, false, false);
329         Assert.assertEquals(scrollX, delegate.getScrollX());
330         Assert.assertEquals(scrollY, delegate.getScrollY());
331         Assert.assertEquals(scrollX, delegate.getNativeScrollX());
332         Assert.assertEquals(scrollY, delegate.getNativeScrollY());
333     }
334 
335     @Test
336     @SmallTest
337     @Feature({"AndroidWebView"})
testDefersScrollUntilTouchEnd()338     public void testDefersScrollUntilTouchEnd() {
339         TestScrollOffsetManagerDelegate delegate = new TestScrollOffsetManagerDelegate();
340         AwScrollOffsetManager offsetManager = new AwScrollOffsetManager(delegate);
341 
342         final int scrollX = 31;
343         final int scrollY = 41;
344 
345         offsetManager.setMaxScrollOffset(MAX_HORIZONTAL_OFFSET, MAX_VERTICAL_OFFSET);
346         offsetManager.setContainerViewSize(VIEW_WIDTH, VIEW_HEIGHT);
347 
348         offsetManager.setProcessingTouchEvent(true);
349         offsetManager.onContainerViewOverScrolled(scrollX, scrollY, false, false);
350         Assert.assertEquals(scrollX, delegate.getScrollX());
351         Assert.assertEquals(scrollY, delegate.getScrollY());
352         Assert.assertEquals(0, delegate.getNativeScrollX());
353         Assert.assertEquals(0, delegate.getNativeScrollY());
354 
355         offsetManager.setProcessingTouchEvent(false);
356         Assert.assertEquals(scrollX, delegate.getScrollX());
357         Assert.assertEquals(scrollY, delegate.getScrollY());
358         Assert.assertEquals(scrollX, delegate.getNativeScrollX());
359         Assert.assertEquals(scrollY, delegate.getNativeScrollY());
360     }
361 
362     @Test
363     @SmallTest
364     @Feature({"AndroidWebView"})
testRequestChildRectangleOnScreenDontScrollIfAlreadyThere()365     public void testRequestChildRectangleOnScreenDontScrollIfAlreadyThere() {
366         TestScrollOffsetManagerDelegate delegate = new TestScrollOffsetManagerDelegate();
367         AwScrollOffsetManager offsetManager = new AwScrollOffsetManager(delegate);
368 
369         offsetManager.setMaxScrollOffset(MAX_HORIZONTAL_OFFSET, MAX_VERTICAL_OFFSET);
370         offsetManager.setContainerViewSize(VIEW_WIDTH, VIEW_HEIGHT);
371 
372         offsetManager.requestChildRectangleOnScreen(
373                 0, 0, new Rect(0, 0, VIEW_WIDTH / 4, VIEW_HEIGHT / 4), true);
374         Assert.assertEquals(0, delegate.getOverScrollDeltaX());
375         Assert.assertEquals(0, delegate.getOverScrollDeltaY());
376         Assert.assertEquals(0, delegate.getScrollX());
377         Assert.assertEquals(0, delegate.getScrollY());
378 
379         offsetManager.requestChildRectangleOnScreen(3 * VIEW_WIDTH / 4, 3 * VIEW_HEIGHT / 4,
380                 new Rect(0, 0, VIEW_WIDTH / 4, VIEW_HEIGHT / 4), true);
381         Assert.assertEquals(0, delegate.getOverScrollDeltaX());
382         Assert.assertEquals(0, delegate.getOverScrollDeltaY());
383         Assert.assertEquals(0, delegate.getScrollX());
384         Assert.assertEquals(0, delegate.getScrollY());
385     }
386 
387     @Test
388     @SmallTest
389     @Feature({"AndroidWebView"})
testRequestChildRectangleOnScreenScrollToBottom()390     public void testRequestChildRectangleOnScreenScrollToBottom() {
391         TestScrollOffsetManagerDelegate delegate = new TestScrollOffsetManagerDelegate();
392         AwScrollOffsetManager offsetManager = new AwScrollOffsetManager(delegate);
393 
394         final int rectWidth = 2;
395         final int rectHeight = 3;
396 
397         offsetManager.setMaxScrollOffset(MAX_HORIZONTAL_OFFSET, MAX_VERTICAL_OFFSET);
398         offsetManager.setContainerViewSize(VIEW_WIDTH, VIEW_HEIGHT);
399 
400         offsetManager.requestChildRectangleOnScreen(CONTENT_WIDTH - rectWidth,
401                 CONTENT_HEIGHT - rectHeight, new Rect(0, 0, rectWidth, rectHeight), true);
402         simlateOverScrollPropagation(offsetManager, delegate);
403         Assert.assertEquals(MAX_HORIZONTAL_OFFSET, delegate.getOverScrollDeltaX());
404         Assert.assertEquals(
405                 CONTENT_HEIGHT - rectHeight - VIEW_HEIGHT / 3, delegate.getOverScrollDeltaY());
406         Assert.assertEquals(MAX_HORIZONTAL_OFFSET, delegate.getScrollX());
407         Assert.assertEquals(MAX_VERTICAL_OFFSET, delegate.getScrollY());
408     }
409 
410     @Test
411     @SmallTest
412     @Feature({"AndroidWebView"})
testRequestChildRectangleOnScreenScrollToBottomLargeRect()413     public void testRequestChildRectangleOnScreenScrollToBottomLargeRect() {
414         TestScrollOffsetManagerDelegate delegate = new TestScrollOffsetManagerDelegate();
415         AwScrollOffsetManager offsetManager = new AwScrollOffsetManager(delegate);
416 
417         final int rectWidth = VIEW_WIDTH;
418         final int rectHeight = VIEW_HEIGHT;
419 
420         offsetManager.setMaxScrollOffset(MAX_HORIZONTAL_OFFSET, MAX_VERTICAL_OFFSET);
421         offsetManager.setContainerViewSize(VIEW_WIDTH, VIEW_HEIGHT);
422 
423         offsetManager.requestChildRectangleOnScreen(CONTENT_WIDTH - rectWidth,
424                 CONTENT_HEIGHT - rectHeight, new Rect(0, 0, rectWidth, rectHeight), true);
425         simlateOverScrollPropagation(offsetManager, delegate);
426         Assert.assertEquals(MAX_HORIZONTAL_OFFSET, delegate.getOverScrollDeltaX());
427         Assert.assertEquals(MAX_VERTICAL_OFFSET, delegate.getOverScrollDeltaY());
428         Assert.assertEquals(MAX_HORIZONTAL_OFFSET, delegate.getScrollX());
429         Assert.assertEquals(MAX_VERTICAL_OFFSET, delegate.getScrollY());
430     }
431 
432     @Test
433     @SmallTest
434     @Feature({"AndroidWebView"})
testRequestChildRectangleOnScreenScrollToTop()435     public void testRequestChildRectangleOnScreenScrollToTop() {
436         TestScrollOffsetManagerDelegate delegate = new TestScrollOffsetManagerDelegate();
437         AwScrollOffsetManager offsetManager = new AwScrollOffsetManager(delegate);
438 
439         final int rectWidth = 2;
440         final int rectHeight = 3;
441 
442         offsetManager.setMaxScrollOffset(MAX_HORIZONTAL_OFFSET, MAX_VERTICAL_OFFSET);
443         offsetManager.setContainerViewSize(VIEW_WIDTH, VIEW_HEIGHT);
444         simulateScrolling(
445                 offsetManager, delegate, CONTENT_WIDTH - VIEW_WIDTH, CONTENT_HEIGHT - VIEW_HEIGHT);
446 
447         offsetManager.requestChildRectangleOnScreen(
448                 0, 0, new Rect(0, 0, rectWidth, rectHeight), true);
449         simlateOverScrollPropagation(offsetManager, delegate);
450         Assert.assertEquals(-CONTENT_WIDTH + VIEW_WIDTH, delegate.getOverScrollDeltaX());
451         Assert.assertEquals(-CONTENT_HEIGHT + VIEW_HEIGHT, delegate.getOverScrollDeltaY());
452         Assert.assertEquals(0, delegate.getScrollX());
453         Assert.assertEquals(0, delegate.getScrollX());
454     }
455 }
456