1 // Copyright 2020 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.omnibox.suggestions;
6 
7 import static org.chromium.base.test.util.CriteriaHelper.DEFAULT_POLLING_INTERVAL;
8 import static org.chromium.chrome.browser.multiwindow.MultiWindowTestHelper.moveActivityToFront;
9 import static org.chromium.chrome.browser.multiwindow.MultiWindowTestHelper.waitForSecondChromeTabbedActivity;
10 
11 import android.app.Activity;
12 import android.app.Instrumentation;
13 import android.app.Instrumentation.ActivityMonitor;
14 import android.content.Intent;
15 import android.os.Build;
16 import android.support.test.InstrumentationRegistry;
17 import android.text.TextUtils;
18 import android.view.ViewGroup;
19 import android.widget.ImageView;
20 import android.widget.TextView;
21 
22 import androidx.test.filters.MediumTest;
23 
24 import org.hamcrest.Matchers;
25 import org.junit.After;
26 import org.junit.Assert;
27 import org.junit.Before;
28 import org.junit.Rule;
29 import org.junit.Test;
30 import org.junit.runner.RunWith;
31 
32 import org.chromium.base.ActivityState;
33 import org.chromium.base.ThreadUtils;
34 import org.chromium.base.test.util.CommandLineFlags;
35 import org.chromium.base.test.util.Criteria;
36 import org.chromium.base.test.util.CriteriaHelper;
37 import org.chromium.base.test.util.CriteriaNotSatisfiedException;
38 import org.chromium.base.test.util.MinAndroidSdkLevel;
39 import org.chromium.chrome.R;
40 import org.chromium.chrome.browser.ChromeTabbedActivity;
41 import org.chromium.chrome.browser.ChromeTabbedActivity2;
42 import org.chromium.chrome.browser.flags.ChromeSwitches;
43 import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
44 import org.chromium.chrome.browser.omnibox.LocationBarLayout;
45 import org.chromium.chrome.browser.omnibox.UrlBar;
46 import org.chromium.chrome.browser.omnibox.suggestions.base.BaseSuggestionView;
47 import org.chromium.chrome.browser.searchwidget.SearchActivity;
48 import org.chromium.chrome.browser.searchwidget.SearchWidgetProvider;
49 import org.chromium.chrome.browser.tab.Tab;
50 import org.chromium.chrome.test.ChromeActivityTestRule;
51 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
52 import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
53 import org.chromium.chrome.test.util.ChromeTabUtils;
54 import org.chromium.chrome.test.util.MenuUtils;
55 import org.chromium.chrome.test.util.OmniboxTestUtils;
56 import org.chromium.chrome.test.util.WaitForFocusHelper;
57 import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
58 import org.chromium.content_public.browser.test.util.TestThreadUtils;
59 import org.chromium.content_public.browser.test.util.TestTouchUtils;
60 import org.chromium.net.test.EmbeddedTestServer;
61 import org.chromium.net.test.ServerCertificate;
62 
63 import java.util.List;
64 
65 /**
66  * Tests of the Switch To Tab feature.
67  */
68 @RunWith(ChromeJUnit4ClassRunner.class)
69 @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
70 public class SwitchToTabTest {
71     @Rule
72     public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
73     private static final int INVALID_INDEX = -1;
74     private static final long SEARCH_ACTIVITY_MAX_TIME_TO_POLL = 10000L;
75 
76     private EmbeddedTestServer mTestServer;
77 
78     @Before
setUp()79     public void setUp() throws InterruptedException {
80         mActivityTestRule.startMainActivityOnBlankPage();
81     }
82 
83     @After
tearDown()84     public void tearDown() {
85         if (mTestServer != null) {
86             mTestServer.stopAndDestroyServer();
87         }
88     }
89 
90     /**
91      * Type the |text| into |activity|'s url_bar.
92      *
93      * @param activity The Activity which url_bar is in.
94      * @param text The text will be typed into url_bar.
95      */
typeInOmnibox(Activity activity, String text)96     private void typeInOmnibox(Activity activity, String text) throws InterruptedException {
97         final UrlBar urlBar = activity.findViewById(R.id.url_bar);
98         Assert.assertNotNull(urlBar);
99 
100         WaitForFocusHelper.acquireFocusForView(urlBar);
101         OmniboxTestUtils.waitForFocusAndKeyboardActive(urlBar, true);
102 
103         TestThreadUtils.runOnUiThreadBlocking(() -> { urlBar.setText(text); });
104     }
105 
106     /**
107      * Type the |text| into |activity|'s URL bar, and wait for switch to tab suggestion shows up.
108      *
109      * @param activity The Activity which URL bar is in.
110      * @param locationBarLayout The layout which omnibox suggestions will show in.
111      * @param tab The tab will be switched to.
112      */
typeAndClickMatchingTabMatchSuggestion(Activity activity, LocationBarLayout locationBarLayout, Tab tab)113     private void typeAndClickMatchingTabMatchSuggestion(Activity activity,
114             LocationBarLayout locationBarLayout, Tab tab) throws InterruptedException {
115         typeInOmnibox(activity, ChromeTabUtils.getTitleOnUiThread(tab));
116 
117         OmniboxTestUtils.waitForOmniboxSuggestions(locationBarLayout);
118         // waitForOmniboxSuggestions only wait until one suggestion shows up, we need to wait util
119         // autocomplete return more suggestions.
120         CriteriaHelper.pollUiThread(() -> {
121             OmniboxSuggestion matchSuggestion =
122                     findTabMatchOmniboxSuggestion(locationBarLayout, tab);
123             Criteria.checkThat(matchSuggestion, Matchers.notNullValue());
124 
125             OmniboxSuggestionsDropdown suggestionsDropdown =
126                     locationBarLayout.getAutocompleteCoordinator().getSuggestionsDropdownForTest();
127 
128             // Make sure data populated to UI
129             int index = findIndexOfTabMatchSuggestionView(suggestionsDropdown, matchSuggestion);
130             Criteria.checkThat(index, Matchers.not(INVALID_INDEX));
131 
132             try {
133                 clickSuggestionActionAt(suggestionsDropdown, index);
134             } catch (InterruptedException e) {
135                 throw new CriteriaNotSatisfiedException(e);
136             }
137         }, SEARCH_ACTIVITY_MAX_TIME_TO_POLL, DEFAULT_POLLING_INTERVAL);
138     }
139 
140     /**
141      * Find the switch to tab suggestion which suggests the |tab|, and return the
142      * suggestion. This method needs to run on the UI thread.
143      *
144      * @param locationBarLayout The layout which omnibox suggestions will show in.
145      * @param tab The tab which the OmniboxSuggestion should suggest.
146      * @return The suggesstion which suggests the |tab|.
147      */
findTabMatchOmniboxSuggestion( LocationBarLayout locationBarLayout, Tab tab)148     private OmniboxSuggestion findTabMatchOmniboxSuggestion(
149             LocationBarLayout locationBarLayout, Tab tab) {
150         ThreadUtils.assertOnUiThread();
151 
152         AutocompleteCoordinator coordinator = locationBarLayout.getAutocompleteCoordinator();
153         // Find the first matching suggestion.
154         for (int i = 0; i < coordinator.getSuggestionCount(); ++i) {
155             OmniboxSuggestion suggestion = coordinator.getSuggestionAt(i);
156             if (suggestion != null && suggestion.hasTabMatch()
157                     && TextUtils.equals(
158                             suggestion.getDescription(), ChromeTabUtils.getTitleOnUiThread(tab))
159                     && TextUtils.equals(suggestion.getUrl().getSpec(), tab.getUrl().getSpec())) {
160                 return suggestion;
161             }
162         }
163         return null;
164     }
165 
166     /**
167      * Find the index of the tab match suggestion in OmniboxSuggestionsDropdown. This method needs
168      * to run on the UI thread.
169      *
170      * @param suggestionsDropdown The OmniboxSuggestionsDropdown contains all the suggestions.
171      * @param suggestion The OmniboxSuggestion we are looking for in the view.
172      * @return The matching suggestion's index.
173      */
findIndexOfTabMatchSuggestionView( OmniboxSuggestionsDropdown suggestionsDropdown, OmniboxSuggestion suggestion)174     private int findIndexOfTabMatchSuggestionView(
175             OmniboxSuggestionsDropdown suggestionsDropdown, OmniboxSuggestion suggestion) {
176         ThreadUtils.assertOnUiThread();
177 
178         ViewGroup viewGroup = suggestionsDropdown.getViewGroup();
179         if (viewGroup == null) {
180             return INVALID_INDEX;
181         }
182 
183         for (int i = 0; i < viewGroup.getChildCount(); i++) {
184             BaseSuggestionView baseSuggestionView = null;
185             try {
186                 baseSuggestionView = (BaseSuggestionView) viewGroup.getChildAt(i);
187             } catch (ClassCastException e) {
188                 continue;
189             }
190 
191             if (baseSuggestionView == null) {
192                 continue;
193             }
194 
195             TextView line1 = baseSuggestionView.findViewById(R.id.line_1);
196             TextView line2 = baseSuggestionView.findViewById(R.id.line_2);
197             if (line1 == null || line2 == null
198                     || !TextUtils.equals(suggestion.getDescription(), line1.getText())
199                     || !TextUtils.equals(suggestion.getDisplayText(), line2.getText())) {
200                 continue;
201             }
202 
203             List<ImageView> buttonsList = baseSuggestionView.getActionButtons();
204             if (buttonsList != null && buttonsList.size() == 1
205                     && TextUtils.equals(baseSuggestionView.getResources().getString(
206                                                 R.string.accessibility_omnibox_switch_to_tab),
207                             buttonsList.get(0).getContentDescription())) {
208                 return i;
209             }
210         }
211 
212         return INVALID_INDEX;
213     }
214 
215     /**
216      * Find the |index|th suggestion in |suggestionsDropdown| and click it.
217      *
218      * @param suggestionsDropdown The omnibox suggestion's dropdown list.
219      * @param index The index of the suggestion tied to click.
220      */
clickSuggestionActionAt(OmniboxSuggestionsDropdown suggestionsDropdown, int index)221     private void clickSuggestionActionAt(OmniboxSuggestionsDropdown suggestionsDropdown, int index)
222             throws InterruptedException {
223         ViewGroup viewGroup = suggestionsDropdown.getViewGroup();
224         BaseSuggestionView baseSuggestionView = (BaseSuggestionView) viewGroup.getChildAt(index);
225         Assert.assertNotNull("Null suggestion for index: " + index, baseSuggestionView);
226 
227         List<ImageView> buttonsList = baseSuggestionView.getActionButtons();
228         Assert.assertNotNull(buttonsList);
229         Assert.assertEquals(buttonsList.size(), 1);
230         TestTouchUtils.performClickOnMainSync(
231                 InstrumentationRegistry.getInstrumentation(), buttonsList.get(0));
232     }
233 
234     /**
235      * Launch the SearchActiviy.
236      */
startSearchActivity()237     private SearchActivity startSearchActivity() {
238         final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
239         ActivityMonitor searchMonitor =
240                 new ActivityMonitor(SearchActivity.class.getName(), null, false);
241         instrumentation.addMonitor(searchMonitor);
242 
243         // Fire the Intent to start up the SearchActivity.
244         Intent intent = new Intent();
245         SearchWidgetProvider.startSearchActivity(intent, /* isVoiceSearch = */ false);
246         Activity searchActivity = instrumentation.waitForMonitorWithTimeout(
247                 searchMonitor, SEARCH_ACTIVITY_MAX_TIME_TO_POLL);
248         Assert.assertNotNull("Activity didn't start", searchActivity);
249         Assert.assertTrue("Wrong activity started", searchActivity instanceof SearchActivity);
250         instrumentation.removeMonitor(searchMonitor);
251         return (SearchActivity) searchActivity;
252     }
253 
254     @Test
255     @MediumTest
256     @EnableFeatures("OmniboxTabSwitchSuggestions")
257     public void
testSwitchToTabSuggestion()258     testSwitchToTabSuggestion() throws InterruptedException {
259         mTestServer = EmbeddedTestServer.createAndStartHTTPSServer(
260                 InstrumentationRegistry.getInstrumentation().getContext(),
261                 ServerCertificate.CERT_OK);
262         final String testHttpsUrl1 = mTestServer.getURL("/chrome/test/data/android/about.html");
263         final String testHttpsUrl2 = mTestServer.getURL("/chrome/test/data/android/ok.txt");
264         final String testHttpsUrl3 = mTestServer.getURL("/chrome/test/data/android/test.html");
265         final Tab aboutTab = mActivityTestRule.loadUrlInNewTab(testHttpsUrl1);
266         mActivityTestRule.loadUrlInNewTab(testHttpsUrl2);
267         mActivityTestRule.loadUrlInNewTab(testHttpsUrl3);
268 
269         LocationBarLayout locationBarLayout =
270                 (LocationBarLayout) mActivityTestRule.getActivity().findViewById(R.id.location_bar);
271         typeAndClickMatchingTabMatchSuggestion(
272                 mActivityTestRule.getActivity(), locationBarLayout, aboutTab);
273 
274         CriteriaHelper.pollUiThread(() -> {
275             Tab tab = mActivityTestRule.getActivity().getActivityTab();
276             Criteria.checkThat(tab, Matchers.notNullValue());
277             Criteria.checkThat(tab, Matchers.is(aboutTab));
278             Criteria.checkThat(tab.getUrlString(), Matchers.is(testHttpsUrl1));
279         });
280     }
281 
282     @Test
283     @MediumTest
284     @MinAndroidSdkLevel(Build.VERSION_CODES.N)
285     @EnableFeatures("OmniboxTabSwitchSuggestions")
286     @CommandLineFlags.Add(ChromeSwitches.DISABLE_TAB_MERGING_FOR_TESTING)
testSwitchToTabSuggestionWhenIncognitoTabOnTop()287     public void testSwitchToTabSuggestionWhenIncognitoTabOnTop() throws InterruptedException {
288         mTestServer = EmbeddedTestServer.createAndStartHTTPSServer(
289                 InstrumentationRegistry.getInstrumentation().getContext(),
290                 ServerCertificate.CERT_OK);
291         final String testHttpsUrl1 = mTestServer.getURL("/chrome/test/data/android/about.html");
292         final String testHttpsUrl2 = mTestServer.getURL("/chrome/test/data/android/ok.txt");
293         final String testHttpsUrl3 = mTestServer.getURL("/chrome/test/data/android/test.html");
294         mActivityTestRule.loadUrlInNewTab(testHttpsUrl2);
295         mActivityTestRule.loadUrlInNewTab(testHttpsUrl3);
296         final Tab aboutTab = mActivityTestRule.loadUrlInNewTab(testHttpsUrl1);
297 
298         // Move "about.html" page to cta2 and create an incognito tab on top of "about.html".
299         final ChromeTabbedActivity cta1 = mActivityTestRule.getActivity();
300         MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true);
301         MenuUtils.invokeCustomMenuActionSync(InstrumentationRegistry.getInstrumentation(), cta1,
302                 R.id.move_to_other_window_menu_id);
303         final ChromeTabbedActivity2 cta2 = waitForSecondChromeTabbedActivity();
304         ChromeActivityTestRule.waitForActivityNativeInitializationComplete(cta2);
305         ChromeTabUtils.newTabFromMenu(InstrumentationRegistry.getInstrumentation(), cta2,
306                 true /*incognito*/, false /*waitForNtpLoad*/);
307         moveActivityToFront(cta1);
308 
309         // Switch back to cta1, and try to switch to "about.html" in cta2.
310         LocationBarLayout locationBarLayout =
311                 (LocationBarLayout) cta1.findViewById(R.id.location_bar);
312         typeAndClickMatchingTabMatchSuggestion(cta1, locationBarLayout, aboutTab);
313 
314         CriteriaHelper.pollUiThread(() -> {
315             Tab tab = cta2.getActivityTab();
316             Criteria.checkThat(tab, Matchers.notNullValue());
317             Criteria.checkThat(tab, Matchers.is(aboutTab));
318             Criteria.checkThat(tab.getUrlString(), Matchers.is(testHttpsUrl1));
319         });
320     }
321 
322     @Test
323     @MediumTest
324     @EnableFeatures("OmniboxTabSwitchSuggestions")
testNoSwitchToIncognitoTabFromNormalModel()325     public void testNoSwitchToIncognitoTabFromNormalModel() throws InterruptedException {
326         mTestServer = EmbeddedTestServer.createAndStartHTTPSServer(
327                 InstrumentationRegistry.getInstrumentation().getContext(),
328                 ServerCertificate.CERT_OK);
329         final String testHttpsUrl1 = mTestServer.getURL("/chrome/test/data/android/about.html");
330         final String testHttpsUrl2 = mTestServer.getURL("/chrome/test/data/android/ok.txt");
331         final String testHttpsUrl3 = mTestServer.getURL("/chrome/test/data/android/test.html");
332         // Open the url trying to match in incognito mode.
333         final Tab aboutTab = mActivityTestRule.loadUrlInNewTab(testHttpsUrl1, true);
334         mActivityTestRule.loadUrlInNewTab(testHttpsUrl2);
335         mActivityTestRule.loadUrlInNewTab(testHttpsUrl3);
336 
337         LocationBarLayout locationBarLayout =
338                 (LocationBarLayout) mActivityTestRule.getActivity().findViewById(R.id.location_bar);
339         // trying to match incognito tab.
340         mActivityTestRule.typeInOmnibox("about", false);
341         OmniboxTestUtils.waitForOmniboxSuggestions(locationBarLayout);
342 
343         CriteriaHelper.pollUiThread(() -> {
344             OmniboxSuggestion matchSuggestion =
345                     findTabMatchOmniboxSuggestion(locationBarLayout, aboutTab);
346             Criteria.checkThat(matchSuggestion, Matchers.nullValue());
347         });
348     }
349 
350     @Test
351     @MediumTest
352     @EnableFeatures("OmniboxTabSwitchSuggestions")
testSwitchToTabInSearchActivity()353     public void testSwitchToTabInSearchActivity() throws InterruptedException {
354         mTestServer = EmbeddedTestServer.createAndStartHTTPSServer(
355                 InstrumentationRegistry.getInstrumentation().getContext(),
356                 ServerCertificate.CERT_OK);
357         final String testHttpsUrl1 = mTestServer.getURL("/chrome/test/data/android/about.html");
358         final String testHttpsUrl2 = mTestServer.getURL("/chrome/test/data/android/ok.txt");
359         final String testHttpsUrl3 = mTestServer.getURL("/chrome/test/data/android/test.html");
360         final Tab aboutTab = mActivityTestRule.loadUrlInNewTab(testHttpsUrl1);
361         mActivityTestRule.loadUrlInNewTab(testHttpsUrl2);
362         mActivityTestRule.loadUrlInNewTab(testHttpsUrl3);
363         Assert.assertNotEquals(mActivityTestRule.getActivity().getActivityTab(), aboutTab);
364 
365         final SearchActivity searchActivity = startSearchActivity();
366         CriteriaHelper.pollUiThread(() -> {
367             Tab tab = mActivityTestRule.getActivity().getActivityTab();
368             Criteria.checkThat(tab, Matchers.notNullValue());
369 
370             // Make sure chrome fully in background.
371             Criteria.checkThat(tab.getWindowAndroid().getActivityState(),
372                     Matchers.isOneOf(ActivityState.STOPPED, ActivityState.DESTROYED));
373         }, SEARCH_ACTIVITY_MAX_TIME_TO_POLL, DEFAULT_POLLING_INTERVAL);
374 
375         final LocationBarLayout locationBarLayout =
376                 (LocationBarLayout) searchActivity.findViewById(R.id.search_location_bar);
377         typeAndClickMatchingTabMatchSuggestion(searchActivity, locationBarLayout, aboutTab);
378 
379         CriteriaHelper.pollUiThread(() -> {
380             Tab tab = mActivityTestRule.getActivity().getActivityTab();
381             Criteria.checkThat(tab, Matchers.notNullValue());
382             Criteria.checkThat(tab, Matchers.is(aboutTab));
383             Criteria.checkThat(tab.getUrlString(), Matchers.is(testHttpsUrl1));
384             // Make sure tab is loaded and in foreground.
385             Criteria.checkThat(
386                     tab.getWindowAndroid().getActivityState(), Matchers.is(ActivityState.RESUMED));
387             Assert.assertEquals(tab, aboutTab);
388         }, SEARCH_ACTIVITY_MAX_TIME_TO_POLL, DEFAULT_POLLING_INTERVAL);
389     }
390 }
391