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