1 // Copyright 2019 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.chrome.browser.gesturenav;
6 
7 import android.app.Activity;
8 import android.graphics.Point;
9 import android.support.test.InstrumentationRegistry;
10 import android.util.DisplayMetrics;
11 
12 import androidx.test.filters.SmallTest;
13 
14 import org.hamcrest.Matchers;
15 import org.junit.After;
16 import org.junit.Assert;
17 import org.junit.Before;
18 import org.junit.Rule;
19 import org.junit.Test;
20 import org.junit.runner.RunWith;
21 
22 import org.chromium.base.ActivityState;
23 import org.chromium.base.ApplicationStatus;
24 import org.chromium.base.test.util.CommandLineFlags;
25 import org.chromium.base.test.util.Criteria;
26 import org.chromium.base.test.util.CriteriaHelper;
27 import org.chromium.base.test.util.DisabledTest;
28 import org.chromium.base.test.util.Restriction;
29 import org.chromium.chrome.browser.compositor.layouts.OverviewModeController;
30 import org.chromium.chrome.browser.flags.ChromeSwitches;
31 import org.chromium.chrome.browser.layouts.animation.CompositorAnimationHandler;
32 import org.chromium.chrome.browser.tab.Tab;
33 import org.chromium.chrome.browser.tab.TabLaunchType;
34 import org.chromium.chrome.browser.tabbed_mode.TabbedRootUiCoordinator;
35 import org.chromium.chrome.browser.tabmodel.TabCreator;
36 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
37 import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
38 import org.chromium.chrome.test.util.ChromeTabUtils;
39 import org.chromium.components.embedder_support.util.UrlConstants;
40 import org.chromium.content_public.browser.LoadUrlParams;
41 import org.chromium.content_public.browser.test.util.TestThreadUtils;
42 import org.chromium.content_public.common.ContentUrlConstants;
43 import org.chromium.net.test.EmbeddedTestServer;
44 import org.chromium.ui.base.PageTransition;
45 import org.chromium.ui.test.util.UiRestriction;
46 
47 /**
48  * Tests {@link NavigationHandler} navigating back/forward using overscroll history navigation.
49  */
50 @RunWith(ChromeJUnit4ClassRunner.class)
51 @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
52         "enable-features=OverscrollHistoryNavigation<GestureNavigation",
53         "force-fieldtrials=GestureNavigation/Enabled",
54         "force-fieldtrial-params=GestureNavigation.Enabled:"
55                 + "gesture_navigation_triggering_area_width/36"})
56 public class NavigationHandlerTest {
57     private static final String RENDERED_PAGE = "/chrome/test/data/android/navigate/simple.html";
58     private static final boolean LEFT_EDGE = true;
59     private static final boolean RIGHT_EDGE = false;
60     private static final int PAGELOAD_TIMEOUT_MS = 4000;
61 
62     private EmbeddedTestServer mTestServer;
63     private HistoryNavigationLayout mNavigationLayout;
64     private NavigationHandler mNavigationHandler;
65     private float mEdgeWidthPx;
66 
67     @Rule
68     public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
69 
70     @Before
setUp()71     public void setUp() throws InterruptedException {
72         mActivityTestRule.startMainActivityOnBlankPage();
73         CompositorAnimationHandler.setTestingMode(true);
74         DisplayMetrics displayMetrics = new DisplayMetrics();
75         mActivityTestRule.getActivity().getWindowManager().getDefaultDisplay().getMetrics(
76                 displayMetrics);
77         mEdgeWidthPx = displayMetrics.density * NavigationHandler.DEFAULT_EDGE_WIDTH_DP;
78         HistoryNavigationCoordinator coordinator = getNavigationCoordinator();
79         mNavigationLayout = coordinator.getLayoutForTesting();
80         mNavigationHandler = coordinator.getNavigationHandlerForTesting();
81     }
82 
83     @After
tearDown()84     public void tearDown() {
85         if (mTestServer != null) mTestServer.stopAndDestroyServer();
86     }
87 
getNavigationCoordinator()88     private HistoryNavigationCoordinator getNavigationCoordinator() {
89         TabbedRootUiCoordinator uiCoordinator =
90                 (TabbedRootUiCoordinator) mActivityTestRule.getActivity()
91                         .getRootUiCoordinatorForTesting();
92         return uiCoordinator.getHistoryNavigationCoordinatorForTesting();
93     }
94 
currentTab()95     private Tab currentTab() {
96         return mActivityTestRule.getActivity().getActivityTabProvider().get();
97     }
98 
loadNewTabPage()99     private void loadNewTabPage() {
100         ChromeTabUtils.newTabFromMenu(InstrumentationRegistry.getInstrumentation(),
101                 mActivityTestRule.getActivity(), false, true);
102     }
103 
assertNavigateOnSwipeFrom(boolean edge, String toUrl)104     private void assertNavigateOnSwipeFrom(boolean edge, String toUrl) {
105         ChromeTabUtils.waitForTabPageLoaded(currentTab(), toUrl, () -> swipeFromEdge(edge), 10);
106         CriteriaHelper.pollUiThread(
107                 ()
108                         -> Criteria.checkThat(ChromeTabUtils.getUrlStringOnUiThread(currentTab()),
109                                 Matchers.is(toUrl)));
110         Assert.assertEquals(
111                 "Didn't navigate back", toUrl, ChromeTabUtils.getUrlStringOnUiThread(currentTab()));
112     }
113 
swipeFromEdge(boolean leftEdge)114     private void swipeFromEdge(boolean leftEdge) {
115         Point size = new Point();
116         mActivityTestRule.getActivity().getWindowManager().getDefaultDisplay().getSize(size);
117         final float startx = leftEdge ? mEdgeWidthPx / 2 : size.x - mEdgeWidthPx / 2;
118         final float endx = size.x / 2;
119         final float yMiddle = size.y / 2;
120         swipe(leftEdge, startx, endx, yMiddle);
121     }
122 
123     // Make an edge swipe too short to trigger the navigation.
shortSwipeFromEdge(boolean leftEdge)124     private void shortSwipeFromEdge(boolean leftEdge) {
125         Point size = new Point();
126         mActivityTestRule.getActivity().getWindowManager().getDefaultDisplay().getSize(size);
127         final float startx = leftEdge ? 0 : size.x;
128         final float endx = leftEdge ? mEdgeWidthPx : size.x - mEdgeWidthPx;
129         final float yMiddle = size.y / 2;
130         swipe(leftEdge, startx, endx, yMiddle);
131     }
132 
swipe(boolean leftEdge, float startx, float endx, float y)133     private void swipe(boolean leftEdge, float startx, float endx, float y) {
134         // # of pixels (of reasonally small value) which a finger moves across
135         // per one motion event.
136         final float distancePx = 6.0f;
137         final float step = Math.signum(endx - startx) * distancePx;
138         final int eventCounts = (int) ((endx - startx) / step);
139 
140         TestThreadUtils.runOnUiThreadBlocking(() -> {
141             mNavigationHandler.onDown();
142             float nextx = startx + step;
143             for (int i = 0; i < eventCounts; i++, nextx += step) {
144                 mNavigationHandler.onScroll(startx, -step, 0, nextx, y);
145             }
146             mNavigationHandler.release(true);
147         });
148     }
149 
150     @Test
151     @SmallTest
testShortSwipeDoesNotTriggerNavigation()152     public void testShortSwipeDoesNotTriggerNavigation() {
153         mActivityTestRule.loadUrl(UrlConstants.NTP_URL);
154         shortSwipeFromEdge(LEFT_EDGE);
155         CriteriaHelper.pollUiThread(mNavigationLayout::isLayoutDetached,
156                 "Navigation Layout should be detached after use");
157         Assert.assertEquals("Current page should not change", UrlConstants.NTP_URL,
158                 ChromeTabUtils.getUrlStringOnUiThread(currentTab()));
159     }
160 
161     @Test
162     @SmallTest
testCloseChromeAtHistoryStackHead()163     public void testCloseChromeAtHistoryStackHead() {
164         loadNewTabPage();
165         final Activity activity = mActivityTestRule.getActivity();
166         swipeFromEdge(LEFT_EDGE);
167         CriteriaHelper.pollUiThread(() -> {
168             int state = ApplicationStatus.getStateForActivity(activity);
169             return state == ActivityState.STOPPED || state == ActivityState.DESTROYED;
170         }, "Chrome should be in background");
171     }
172 
173     @Test
174     @SmallTest
testLayoutGetsDetachedAfterUse()175     public void testLayoutGetsDetachedAfterUse() {
176         mActivityTestRule.loadUrl(UrlConstants.NTP_URL);
177         mActivityTestRule.loadUrl(UrlConstants.RECENT_TABS_URL);
178         swipeFromEdge(LEFT_EDGE);
179         CriteriaHelper.pollUiThread(mNavigationLayout::isLayoutDetached,
180                 "Navigation Layout should be detached after use");
181         Assert.assertNull(mNavigationLayout.getDetachLayoutRunnable());
182     }
183 
184     @Test
185     @SmallTest
testReleaseGlowWithoutPrecedingPullIgnored()186     public void testReleaseGlowWithoutPrecedingPullIgnored() {
187         mTestServer = EmbeddedTestServer.createAndStartServer(
188                 InstrumentationRegistry.getInstrumentation().getContext());
189         mActivityTestRule.loadUrl(mTestServer.getURL(RENDERED_PAGE));
190         TestThreadUtils.runOnUiThreadBlocking(() -> {
191             // Right swipe on a rendered page to initiate overscroll glow.
192             mNavigationHandler.onDown();
193             mNavigationHandler.triggerUi(true, 0, 0);
194 
195             // Test that a release without preceding pull requests works
196             // without crashes.
197             mNavigationHandler.release(true);
198         });
199 
200         // Just check we're still on the same URL.
201         Assert.assertEquals(mTestServer.getURL(RENDERED_PAGE),
202                 ChromeTabUtils.getUrlStringOnUiThread(currentTab()));
203     }
204 
205     @Test
206     @SmallTest
testSwipeNavigateOnNativePage()207     public void testSwipeNavigateOnNativePage() {
208         mActivityTestRule.loadUrl(UrlConstants.NTP_URL);
209         mActivityTestRule.loadUrl(UrlConstants.RECENT_TABS_URL);
210         assertNavigateOnSwipeFrom(LEFT_EDGE, UrlConstants.NTP_URL);
211         assertNavigateOnSwipeFrom(RIGHT_EDGE, UrlConstants.RECENT_TABS_URL);
212     }
213 
214     @Test
215     @SmallTest
testSwipeNavigateOnRenderedPage()216     public void testSwipeNavigateOnRenderedPage() {
217         mTestServer = EmbeddedTestServer.createAndStartServer(
218                 InstrumentationRegistry.getInstrumentation().getContext());
219         mActivityTestRule.loadUrl(mTestServer.getURL(RENDERED_PAGE));
220         mActivityTestRule.loadUrl(ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
221 
222         assertNavigateOnSwipeFrom(LEFT_EDGE, mTestServer.getURL(RENDERED_PAGE));
223         assertNavigateOnSwipeFrom(RIGHT_EDGE, ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
224     }
225 
226     @Test
227     @SmallTest
testLeftEdgeSwipeClosesTabLaunchedFromLink()228     public void testLeftEdgeSwipeClosesTabLaunchedFromLink() {
229         Tab oldTab = currentTab();
230         TabCreator tabCreator = mActivityTestRule.getActivity().getTabCreator(false);
231         Tab newTab = TestThreadUtils.runOnUiThreadBlockingNoException(() -> {
232             return tabCreator.createNewTab(
233                     new LoadUrlParams(UrlConstants.RECENT_TABS_URL, PageTransition.LINK),
234                     TabLaunchType.FROM_LINK, oldTab);
235         });
236         Assert.assertEquals(newTab, currentTab());
237         swipeFromEdge(LEFT_EDGE);
238 
239         // Assert that the new tab was closed and the old tab is the current tab again.
240         CriteriaHelper.pollUiThread(() -> !newTab.isInitialized());
241         Assert.assertEquals(oldTab, currentTab());
242         Assert.assertEquals("Chrome should remain in foreground", ActivityState.RESUMED,
243                 ApplicationStatus.getStateForActivity(mActivityTestRule.getActivity()));
244     }
245 
246     @Test
247     @SmallTest
248     @DisabledTest(message = "https://crbug.com/1147553")
testSwipeAfterDestroy()249     public void testSwipeAfterDestroy() {
250         mTestServer = EmbeddedTestServer.createAndStartServer(
251                 InstrumentationRegistry.getInstrumentation().getContext());
252         mActivityTestRule.loadUrl(mTestServer.getURL(RENDERED_PAGE));
253         getNavigationCoordinator().destroy();
254 
255         // |triggerUi| can be invoked by SwipeRefreshHandler on the rendered
256         // page. Make sure this won't crash after the coordinator (and also
257         // handler action delegate) is destroyed.
258         Assert.assertFalse(mNavigationHandler.triggerUi(LEFT_EDGE, 0, 0));
259 
260         // Just check we're still on the same URL.
261         Assert.assertEquals(mTestServer.getURL(RENDERED_PAGE),
262                 ChromeTabUtils.getUrlStringOnUiThread(currentTab()));
263     }
264 
265     @Test
266     @SmallTest
267     @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
testEdgeSwipeIsNoopInTabSwitcher()268     public void testEdgeSwipeIsNoopInTabSwitcher() {
269         mActivityTestRule.loadUrl(UrlConstants.NTP_URL);
270         mActivityTestRule.loadUrl(UrlConstants.RECENT_TABS_URL);
271         setTabSwitcherModeAndWait(true);
272         swipeFromEdge(LEFT_EDGE);
273         Assert.assertTrue("Chrome should stay in tab switcher",
274                 mActivityTestRule.getActivity().isInOverviewMode());
275         setTabSwitcherModeAndWait(false);
276         Assert.assertEquals("Current page should not change", UrlConstants.RECENT_TABS_URL,
277                 ChromeTabUtils.getUrlStringOnUiThread(currentTab()));
278     }
279 
280     /**
281      * Enter or exit the tab switcher with animations and wait for the scene to change.
282      * @param inSwitcher Whether to enter or exit the tab switcher.
283      */
setTabSwitcherModeAndWait(boolean inSwitcher)284     private void setTabSwitcherModeAndWait(boolean inSwitcher) {
285         OverviewModeController controller = mActivityTestRule.getActivity().getLayoutManager();
286         if (inSwitcher) {
287             TestThreadUtils.runOnUiThreadBlocking(() -> controller.showOverview(false));
288         } else {
289             TestThreadUtils.runOnUiThreadBlocking(() -> controller.hideOverview(false));
290         }
291     }
292 }
293