1 // Copyright 2015 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.test.util; 6 7 import android.app.Instrumentation; 8 import android.support.test.InstrumentationRegistry; 9 import android.text.TextUtils; 10 import android.view.View; 11 12 import androidx.annotation.Nullable; 13 14 import org.junit.Assert; 15 16 import org.chromium.base.Log; 17 import org.chromium.base.ThreadUtils; 18 import org.chromium.base.test.util.CallbackHelper; 19 import org.chromium.chrome.R; 20 import org.chromium.chrome.browser.ChromeTabbedActivity; 21 import org.chromium.chrome.browser.app.ChromeActivity; 22 import org.chromium.chrome.browser.compositor.layouts.components.CompositorButton; 23 import org.chromium.chrome.browser.compositor.overlays.strip.StripLayoutHelper; 24 import org.chromium.chrome.browser.tab.EmptyTabObserver; 25 import org.chromium.chrome.browser.tab.Tab; 26 import org.chromium.chrome.browser.tab.TabCreationState; 27 import org.chromium.chrome.browser.tab.TabHidingType; 28 import org.chromium.chrome.browser.tab.TabLaunchType; 29 import org.chromium.chrome.browser.tab.TabSelectionType; 30 import org.chromium.chrome.browser.tab.TabWebContentsObserver; 31 import org.chromium.chrome.browser.tabmodel.TabModel; 32 import org.chromium.chrome.browser.tabmodel.TabModelObserver; 33 import org.chromium.chrome.browser.tabmodel.TabModelSelector; 34 import org.chromium.chrome.browser.tabmodel.TabModelUtils; 35 import org.chromium.chrome.test.ChromeTabbedActivityTestRule; 36 import org.chromium.chrome.test.util.browser.TabTitleObserver; 37 import org.chromium.content_public.browser.LoadUrlParams; 38 import org.chromium.content_public.browser.RenderWidgetHostView; 39 import org.chromium.content_public.browser.WebContents; 40 import org.chromium.content_public.browser.test.util.TestThreadUtils; 41 import org.chromium.content_public.browser.test.util.TestTouchUtils; 42 import org.chromium.content_public.browser.test.util.TouchCommon; 43 import org.chromium.url.GURL; 44 45 import java.util.List; 46 import java.util.Locale; 47 import java.util.concurrent.Callable; 48 import java.util.concurrent.CountDownLatch; 49 import java.util.concurrent.ExecutionException; 50 import java.util.concurrent.TimeUnit; 51 import java.util.concurrent.TimeoutException; 52 import java.util.concurrent.atomic.AtomicReference; 53 54 /** 55 * A utility class that contains methods generic to all Tabs tests. 56 */ 57 public class ChromeTabUtils { 58 private static final String TAG = "ChromeTabUtils"; 59 public static final int TITLE_UPDATE_TIMEOUT_SECONDS = 3; 60 61 /** 62 * The required page load percentage for the page to be considered ready assuming the 63 * TextureView is also ready. 64 */ 65 private static final float CONSIDERED_READY_LOAD_PERCENTAGE = 1; 66 67 /** 68 * An observer that waits for a Tab to load a page. 69 * 70 * The observer can be configured to either wait for the Tab to load a specific page 71 * (if expectedUrl is non-null) or any page (otherwise). On seeing the tab finish 72 * a page load or crash, the observer will notify the provided callback and stop 73 * watching the tab. On load stop, the observer will decrement the provided latch 74 * and continue watching the page in case the tab subsequently crashes or finishes 75 * a page load. 76 * 77 * This may seem complicated, but it's intended to handle three distinct cases: 78 * 1) Successful page load + observer starts watching before onPageLoadFinished fires. 79 * This is the most normal case: onPageLoadFinished fires, then onLoadStopped fires, 80 * and we see both. 81 * 2) Crash on page load. onLoadStopped fires, then onCrash fires, and we see both. 82 * 3) Successful page load + observer starts watching after onPageLoadFinished fires. 83 * We miss the onPageLoadFinished and *only* see onLoadStopped. 84 * 85 * Receiving onPageLoadFinished is sufficient to know that we're dealing with scenario #1. 86 * Receiving onCrash is sufficient to know that we're dealing with scenario #2. 87 * Receiving onLoadStopped without a preceding onPageLoadFinished indicates that we're dealing 88 * with either scenario #2 *or* #3, so we have to keep watching for a call to onCrash. 89 */ 90 private static class TabPageLoadedObserver extends EmptyTabObserver { 91 private CallbackHelper mCallback; 92 private String mExpectedUrl; 93 private CountDownLatch mLoadStoppedLatch; 94 TabPageLoadedObserver(CallbackHelper loadCompleteCallback, String expectedUrl, CountDownLatch loadStoppedLatch)95 public TabPageLoadedObserver(CallbackHelper loadCompleteCallback, String expectedUrl, 96 CountDownLatch loadStoppedLatch) { 97 mCallback = loadCompleteCallback; 98 mExpectedUrl = expectedUrl; 99 mLoadStoppedLatch = loadStoppedLatch; 100 } 101 102 @Override onCrash(Tab tab)103 public void onCrash(Tab tab) { 104 mCallback.notifyFailed("Tab crashed :("); 105 tab.removeObserver(this); 106 } 107 108 @Override onLoadStopped(Tab tab, boolean toDifferentDocument)109 public void onLoadStopped(Tab tab, boolean toDifferentDocument) { 110 mLoadStoppedLatch.countDown(); 111 } 112 113 @Override onPageLoadFinished(Tab tab, String url)114 public void onPageLoadFinished(Tab tab, String url) { 115 if (mExpectedUrl == null || TextUtils.equals(url, mExpectedUrl)) { 116 mCallback.notifyCalled(); 117 tab.removeObserver(this); 118 } 119 } 120 } 121 loadComplete(Tab tab, String url)122 private static boolean loadComplete(Tab tab, String url) { 123 return !tab.isLoading() 124 && (url == null || TextUtils.equals(getUrlStringOnUiThread(tab), url)) 125 && !tab.getWebContents().isLoadingToDifferentDocument(); 126 } 127 getTitleOnUiThread(Tab tab)128 public static String getTitleOnUiThread(Tab tab) { 129 AtomicReference<String> res = new AtomicReference<>(); 130 TestThreadUtils.runOnUiThreadBlocking(() -> { res.set(tab.getTitle()); }); 131 return res.get(); 132 } 133 getUrlStringOnUiThread(Tab tab)134 public static String getUrlStringOnUiThread(Tab tab) { 135 AtomicReference<String> res = new AtomicReference<>(); 136 TestThreadUtils.runOnUiThreadBlocking(() -> { res.set(tab.getUrlString()); }); 137 return res.get(); 138 } 139 getUrlOnUiThread(Tab tab)140 public static GURL getUrlOnUiThread(Tab tab) { 141 AtomicReference<GURL> res = new AtomicReference<>(); 142 TestThreadUtils.runOnUiThreadBlocking(() -> { res.set(tab.getUrl()); }); 143 return res.get(); 144 } 145 146 /** 147 * Waits for the given tab to finish loading the given URL, or, if the given URL is 148 * null, waits for the current page to load. 149 * 150 * @param tab The tab to wait for the page loading to be complete. 151 * @param url The URL that will be waited to load for. Pass in null if loading the 152 * current page is sufficient. 153 */ waitForTabPageLoaded(final Tab tab, @Nullable final String url)154 public static void waitForTabPageLoaded(final Tab tab, @Nullable final String url) { 155 waitForTabPageLoaded(tab, url, null, 10L); 156 } 157 158 /** 159 * Waits for the given tab to load the given URL, or, if the given URL is null, waits 160 * for the triggered load to complete. 161 * 162 * @param tab The tab to wait for the page loading to be complete. 163 * @param url The expected url of the loaded page. Pass in null if loading the 164 * current page is sufficient. 165 * @param loadTrigger The trigger action that will result in a page load finished event 166 * to be fired (not run on the UI thread by default). 167 */ waitForTabPageLoaded( final Tab tab, @Nullable final String url, @Nullable Runnable loadTrigger)168 public static void waitForTabPageLoaded( 169 final Tab tab, @Nullable final String url, @Nullable Runnable loadTrigger) { 170 waitForTabPageLoaded(tab, url, loadTrigger, CallbackHelper.WAIT_TIMEOUT_SECONDS); 171 } 172 173 /** 174 * Waits for the given tab to finish loading its current page. 175 * 176 * @param tab The tab to wait for the page loading to be complete. 177 * @param loadTrigger The trigger action that will result in a page load finished event 178 * to be fired (not run on the UI thread by default). 179 * @param secondsToWait The number of seconds to wait for the page to be loaded. 180 */ waitForTabPageLoaded( final Tab tab, Runnable loadTrigger, long secondsToWait)181 public static void waitForTabPageLoaded( 182 final Tab tab, Runnable loadTrigger, long secondsToWait) { 183 waitForTabPageLoaded(tab, null, loadTrigger, secondsToWait); 184 } 185 186 /** 187 * Waits for the given tab to load the given URL, or, if the given URL is null, waits 188 * for the triggered load to complete. 189 * 190 * @param tab The tab to wait for the page loading to be complete. 191 * @param url The expected url of the loaded page. Pass in null if loading the 192 * current page is sufficient. 193 * @param loadTrigger The trigger action that will result in a page load finished event 194 * to be fired (not run on the UI thread by default). Pass in null if the 195 * load is triggered externally. 196 * @param secondsToWait The number of seconds to wait for the page to be loaded. 197 */ waitForTabPageLoaded(final Tab tab, @Nullable final String url, @Nullable Runnable loadTrigger, long secondsToWait)198 public static void waitForTabPageLoaded(final Tab tab, @Nullable final String url, 199 @Nullable Runnable loadTrigger, long secondsToWait) { 200 Assert.assertFalse(ThreadUtils.runningOnUiThread()); 201 202 final CountDownLatch loadStoppedLatch = new CountDownLatch(1); 203 final CallbackHelper loadedCallback = new CallbackHelper(); 204 TestThreadUtils.runOnUiThreadBlocking(() -> { 205 // Don't check for the load being already complete if there is a trigger to run. 206 if (loadTrigger == null && loadComplete(tab, url)) { 207 loadedCallback.notifyCalled(); 208 return; 209 } 210 tab.addObserver(new TabPageLoadedObserver(loadedCallback, url, loadStoppedLatch)); 211 }); 212 if (loadTrigger != null) { 213 loadTrigger.run(); 214 } 215 try { 216 loadedCallback.waitForCallback(0, 1, secondsToWait, TimeUnit.SECONDS); 217 } catch (TimeoutException e) { 218 // In the event that: 219 // 1) the tab is on the correct page 220 // 2) we weren't notified that the page load finished 221 // 3) we *were* notified that the tab stopped loading 222 // 4) the tab didn't crash 223 // 224 // then it's likely the case that we started observing the tab after 225 // onPageLoadFinished but before onLoadStopped. (The latter sets tab.mIsLoading to 226 // false.) Try to carry on with the test. 227 if (loadStoppedLatch.getCount() == 0 && loadComplete(tab, url)) { 228 Log.w(TAG, 229 "onPageLoadFinished was never called, but loading stopped " 230 + "on the expected page. Tentatively continuing."); 231 } else { 232 WebContents webContents = tab.getWebContents(); 233 Assert.fail(String.format(Locale.ENGLISH, 234 "Page did not load. Tab information at time of failure -- " 235 + "expected url: '%s', actual URL: '%s', load progress: %d, is " 236 + "loading: %b, web contents init: %b, web contents loading: %b", 237 url, tab.getUrlString(), Math.round(100 * tab.getProgress()), 238 tab.isLoading(), webContents != null, 239 webContents == null ? false : webContents.isLoadingToDifferentDocument())); 240 } 241 } 242 } 243 244 /** 245 * Waits for the given tab to start loading its current page. 246 * 247 * @param tab The tab to wait for the page loading to be started. 248 * @param expectedUrl The expected url of the started page load. Pass in null if starting 249 * any load is sufficient. 250 * @param loadTrigger The trigger action that will result in a page load started event 251 * to be fired (not run on the UI thread by default). 252 */ waitForTabPageLoadStart( final Tab tab, @Nullable final String expectedUrl, Runnable loadTrigger)253 public static void waitForTabPageLoadStart( 254 final Tab tab, @Nullable final String expectedUrl, Runnable loadTrigger) { 255 waitForTabPageLoadStart(tab, expectedUrl, loadTrigger, CallbackHelper.WAIT_TIMEOUT_SECONDS); 256 } 257 258 /** 259 * Waits for the given tab to start loading its current page. 260 * 261 * @param tab The tab to wait for the page loading to be started. 262 * @param expectedUrl The expected url of the started page load. Pass in null if starting 263 * any load is sufficient. 264 * @param loadTrigger The trigger action that will result in a page load started event 265 * to be fired (not run on the UI thread by default). 266 * @param secondsToWait The number of seconds to wait for the page to be load to be started. 267 */ waitForTabPageLoadStart(final Tab tab, @Nullable final String expectedUrl, Runnable loadTrigger, long secondsToWait)268 public static void waitForTabPageLoadStart(final Tab tab, @Nullable final String expectedUrl, 269 Runnable loadTrigger, long secondsToWait) { 270 final CallbackHelper startedCallback = new CallbackHelper(); 271 TestThreadUtils.runOnUiThreadBlocking(() -> { 272 tab.addObserver(new EmptyTabObserver() { 273 @Override 274 public void onPageLoadStarted(Tab tab, String url) { 275 if (expectedUrl == null || TextUtils.equals(url, expectedUrl)) { 276 startedCallback.notifyCalled(); 277 tab.removeObserver(this); 278 } 279 } 280 }); 281 }); 282 loadTrigger.run(); 283 try { 284 startedCallback.waitForCallback(0, 1, secondsToWait, TimeUnit.SECONDS); 285 } catch (TimeoutException e) { 286 Assert.fail("Page did not start loading. Tab information at time of failure --" 287 + " url: " + tab.getUrlString() + ", load progress: " + tab.getProgress() 288 + ", is loading: " + Boolean.toString(tab.isLoading())); 289 } 290 } 291 292 /** 293 * An observer that waits for a Tab to become interactable. 294 * 295 * Notifies the provided callback when: 296 * - the page has become interactable 297 * - the tab has been hidden and will not become interactable. 298 * Stops observing with a failure if the tab has crashed. 299 * 300 * We treat the hidden case as success to handle loads in which a page immediately closes itself 301 * or opens a new foreground tab (popup), and may not become interactable. 302 */ 303 private static class TabPageInteractableObserver extends EmptyTabObserver { 304 private Tab mTab; 305 private CallbackHelper mCallback; 306 TabPageInteractableObserver(Tab tab, CallbackHelper interactableCallback)307 public TabPageInteractableObserver(Tab tab, CallbackHelper interactableCallback) { 308 mTab = tab; 309 mCallback = interactableCallback; 310 } 311 312 @Override onCrash(Tab tab)313 public void onCrash(Tab tab) { 314 mCallback.notifyFailed("Tab crashed :("); 315 mTab.removeObserver(this); 316 } 317 318 @Override onHidden(Tab tab, @TabHidingType int type)319 public void onHidden(Tab tab, @TabHidingType int type) { 320 mCallback.notifyCalled(); 321 mTab.removeObserver(this); 322 } 323 324 @Override onInteractabilityChanged(Tab tab, boolean interactable)325 public void onInteractabilityChanged(Tab tab, boolean interactable) { 326 if (interactable) { 327 mCallback.notifyCalled(); 328 mTab.removeObserver(this); 329 } 330 } 331 } 332 333 /** 334 * Waits for the tab to become interactable. This occurs after load, once all view 335 * animations have completed. 336 * 337 * @param tab The tab to wait for interactability on. 338 */ waitForInteractable(final Tab tab)339 public static void waitForInteractable(final Tab tab) { 340 Assert.assertFalse(ThreadUtils.runningOnUiThread()); 341 342 final CallbackHelper interactableCallback = new CallbackHelper(); 343 TestThreadUtils.runOnUiThreadBlocking(() -> { 344 // If a tab is hidden, don't wait for interactivity. See note in 345 // TabPageInteractableObserver. 346 if (tab.isUserInteractable() || tab.isHidden()) { 347 interactableCallback.notifyCalled(); 348 return; 349 } 350 tab.addObserver(new TabPageInteractableObserver(tab, interactableCallback)); 351 }); 352 353 try { 354 interactableCallback.waitForCallback(0, 1, 10L, TimeUnit.SECONDS); 355 } catch (TimeoutException e) { 356 Assert.fail("Page never became interactable."); 357 } 358 } 359 360 /** 361 * Switch to the given TabIndex in the current tabModel. 362 * @param tabIndex 363 */ switchTabInCurrentTabModel(final ChromeActivity activity, final int tabIndex)364 public static void switchTabInCurrentTabModel(final ChromeActivity activity, 365 final int tabIndex) { 366 TestThreadUtils.runOnUiThreadBlocking( 367 () -> { TabModelUtils.setIndex(activity.getCurrentTabModel(), tabIndex); }); 368 } 369 370 /** 371 * Simulates a click to the normal (not incognito) new tab button. 372 * <p> 373 * Does not wait for the tab to be loaded. 374 */ clickNewTabButton( Instrumentation instrumentation, ChromeTabbedActivity activity)375 public static void clickNewTabButton( 376 Instrumentation instrumentation, ChromeTabbedActivity activity) { 377 final TabModel normalTabModel = activity.getTabModelSelector().getModel(false); 378 final CallbackHelper createdCallback = new CallbackHelper(); 379 normalTabModel.addObserver(new TabModelObserver() { 380 @Override 381 public void didAddTab( 382 Tab tab, @TabLaunchType int type, @TabCreationState int creationState) { 383 createdCallback.notifyCalled(); 384 normalTabModel.removeObserver(this); 385 } 386 }); 387 // Tablet and phone have different new tab buttons; click the right one. 388 if (activity.isTablet()) { 389 StripLayoutHelper strip = 390 TabStripUtils.getStripLayoutHelper(activity, false /* incognito */); 391 CompositorButton newTabButton = strip.getNewTabButton(); 392 TabStripUtils.clickCompositorButton(newTabButton, instrumentation, activity); 393 instrumentation.waitForIdleSync(); 394 } else { 395 TouchCommon.singleClickView(activity.findViewById(R.id.new_tab_button)); 396 } 397 398 try { 399 createdCallback.waitForCallback(null, 0, 1, 10, TimeUnit.SECONDS); 400 } catch (TimeoutException e) { 401 Assert.fail("Never received tab creation event"); 402 } 403 } 404 405 /** 406 * Creates a new tab by invoking the 'New Tab' menu item. 407 * <p> 408 * Returns when the tab has been created and has finished navigating. 409 */ newTabFromMenu( Instrumentation instrumentation, final ChromeActivity activity)410 public static void newTabFromMenu( 411 Instrumentation instrumentation, final ChromeActivity activity) { 412 newTabFromMenu(instrumentation, activity, false, true); 413 } 414 415 /** 416 * Creates a new tab by invoking the 'New Tab' or 'New Incognito Tab' menu item. 417 * <p> 418 * Returns when the tab has been created and has finished navigating. 419 */ newTabFromMenu(Instrumentation instrumentation, final ChromeActivity activity, boolean incognito, boolean waitForNtpLoad)420 public static void newTabFromMenu(Instrumentation instrumentation, 421 final ChromeActivity activity, boolean incognito, boolean waitForNtpLoad) { 422 final CallbackHelper createdCallback = new CallbackHelper(); 423 final CallbackHelper selectedCallback = new CallbackHelper(); 424 425 TabModel tabModel = activity.getTabModelSelector().getModel(incognito); 426 TabModelObserver observer = new TabModelObserver() { 427 @Override 428 public void didAddTab( 429 Tab tab, @TabLaunchType int type, @TabCreationState int creationState) { 430 createdCallback.notifyCalled(); 431 } 432 433 @Override 434 public void didSelectTab(Tab tab, @TabSelectionType int type, int lastId) { 435 selectedCallback.notifyCalled(); 436 } 437 }; 438 tabModel.addObserver(observer); 439 440 MenuUtils.invokeCustomMenuActionSync(instrumentation, activity, 441 incognito ? R.id.new_incognito_tab_menu_id : R.id.new_tab_menu_id); 442 443 try { 444 createdCallback.waitForCallback(0); 445 } catch (TimeoutException ex) { 446 Assert.fail("Never received tab created event"); 447 } 448 try { 449 selectedCallback.waitForCallback(0); 450 } catch (TimeoutException ex) { 451 Assert.fail("Never received tab selected event"); 452 } 453 tabModel.removeObserver(observer); 454 455 Tab tab = activity.getActivityTab(); 456 waitForTabPageLoaded(tab, (String) null); 457 if (waitForNtpLoad) NewTabPageTestUtils.waitForNtpLoaded(tab); 458 instrumentation.waitForIdleSync(); 459 Log.d(TAG, "newTabFromMenu <<"); 460 } 461 462 /** 463 * New multiple tabs by invoking the 'new' menu item n times. 464 * @param n The number of tabs you want to create. 465 */ newTabsFromMenu( Instrumentation instrumentation, ChromeTabbedActivity activity, int n)466 public static void newTabsFromMenu( 467 Instrumentation instrumentation, ChromeTabbedActivity activity, int n) { 468 while (n > 0) { 469 newTabFromMenu(instrumentation, activity); 470 --n; 471 } 472 } 473 474 /** 475 * Creates a new tab in the specified model then waits for it to load. 476 * <p> 477 * Returns when the tab has been created and finishes loading. 478 */ fullyLoadUrlInNewTab(Instrumentation instrumentation, final ChromeTabbedActivity activity, final String url, final boolean incognito)479 public static void fullyLoadUrlInNewTab(Instrumentation instrumentation, 480 final ChromeTabbedActivity activity, final String url, final boolean incognito) { 481 newTabFromMenu(instrumentation, activity, incognito, false); 482 483 final Tab tab = activity.getActivityTab(); 484 waitForTabPageLoaded(tab, url, new Runnable() { 485 @Override 486 public void run() { 487 loadUrlOnUiThread(tab, url); 488 } 489 }); 490 instrumentation.waitForIdleSync(); 491 } 492 loadUrlOnUiThread(final Tab tab, final String url)493 public static void loadUrlOnUiThread(final Tab tab, final String url) { 494 TestThreadUtils.runOnUiThreadBlocking(() -> { tab.loadUrl(new LoadUrlParams(url)); }); 495 } 496 497 /** 498 * Ensure that at least some given number of tabs are open. 499 */ ensureNumOpenTabs( Instrumentation instrumentation, ChromeTabbedActivity activity, int newCount)500 public static void ensureNumOpenTabs( 501 Instrumentation instrumentation, ChromeTabbedActivity activity, int newCount) { 502 int curCount = getNumOpenTabs(activity); 503 if (curCount < newCount) { 504 newTabsFromMenu(instrumentation, activity, newCount - curCount); 505 } 506 } 507 508 /** 509 * Fetch the number of tabs open in the current model. 510 */ getNumOpenTabs(final ChromeActivity activity)511 public static int getNumOpenTabs(final ChromeActivity activity) { 512 return TestThreadUtils.runOnUiThreadBlockingNoException(new Callable<Integer>() { 513 @Override 514 public Integer call() { 515 return activity.getCurrentTabModel().getCount(); 516 } 517 }); 518 } 519 520 /** 521 * Closes the current tab through TabModelSelector. 522 * <p> 523 * Returns after the tab has been closed. 524 */ 525 public static void closeCurrentTab( 526 final Instrumentation instrumentation, final ChromeActivity activity) { 527 closeTabWithAction(instrumentation, activity, new Runnable() { 528 @Override 529 public void run() { 530 instrumentation.runOnMainSync(new Runnable() { 531 @Override 532 public void run() { 533 TabModelUtils.closeCurrentTab(activity.getCurrentTabModel()); 534 } 535 }); 536 } 537 }); 538 } 539 540 /** 541 * Closes a tab with the given action and waits for a tab closure to be observed. 542 */ 543 public static void closeTabWithAction( 544 Instrumentation instrumentation, final ChromeActivity activity, Runnable action) { 545 final CallbackHelper closeCallback = new CallbackHelper(); 546 final TabModelObserver observer = new TabModelObserver() { 547 @Override 548 public void willCloseTab(Tab tab, boolean animate) { 549 closeCallback.notifyCalled(); 550 } 551 }; 552 instrumentation.runOnMainSync(new Runnable() { 553 @Override 554 public void run() { 555 TabModelSelector selector = activity.getTabModelSelector(); 556 for (TabModel tabModel : selector.getModels()) { 557 tabModel.addObserver(observer); 558 } 559 } 560 }); 561 562 action.run(); 563 564 try { 565 closeCallback.waitForCallback(0); 566 } catch (TimeoutException e) { 567 Assert.fail("Tab closed event was never received"); 568 } 569 instrumentation.runOnMainSync(new Runnable() { 570 @Override 571 public void run() { 572 TabModelSelector selector = activity.getTabModelSelector(); 573 for (TabModel tabModel : selector.getModels()) { 574 tabModel.removeObserver(observer); 575 } 576 } 577 }); 578 instrumentation.waitForIdleSync(); 579 Log.d(TAG, "closeTabWithAction <<"); 580 } 581 582 /** 583 * Close all tabs and waits for all tabs pending closure to be observed. 584 */ 585 public static void closeAllTabs( 586 Instrumentation instrumentation, final ChromeTabbedActivity activity) { 587 final CallbackHelper closeCallback = new CallbackHelper(); 588 final TabModelObserver observer = new TabModelObserver() { 589 @Override 590 public void multipleTabsPendingClosure(List<Tab> tabs, boolean isAllTabs) { 591 closeCallback.notifyCalled(); 592 } 593 }; 594 instrumentation.runOnMainSync(new Runnable() { 595 @Override 596 public void run() { 597 TabModelSelector selector = activity.getTabModelSelector(); 598 for (TabModel tabModel : selector.getModels()) { 599 tabModel.addObserver(observer); 600 } 601 } 602 }); 603 604 TestThreadUtils.runOnUiThreadBlocking( 605 () -> { activity.getTabModelSelector().closeAllTabs(); }); 606 607 try { 608 closeCallback.waitForCallback(0); 609 } catch (TimeoutException e) { 610 Assert.fail("All tabs pending closure event was never received"); 611 } 612 instrumentation.runOnMainSync(new Runnable() { 613 @Override 614 public void run() { 615 TabModelSelector selector = activity.getTabModelSelector(); 616 for (TabModel tabModel : selector.getModels()) { 617 tabModel.removeObserver(observer); 618 } 619 } 620 }); 621 instrumentation.waitForIdleSync(); 622 } 623 624 /** 625 * Selects a tab with the given action and waits for the selection event to be observed. 626 */ 627 public static void selectTabWithAction( 628 Instrumentation instrumentation, final ChromeTabbedActivity activity, Runnable action) { 629 final CallbackHelper selectCallback = new CallbackHelper(); 630 final TabModelObserver observer = new TabModelObserver() { 631 @Override 632 public void didSelectTab(Tab tab, @TabSelectionType int type, int lastId) { 633 selectCallback.notifyCalled(); 634 } 635 }; 636 instrumentation.runOnMainSync(new Runnable() { 637 @Override 638 public void run() { 639 TabModelSelector selector = activity.getTabModelSelector(); 640 for (TabModel tabModel : selector.getModels()) { 641 tabModel.addObserver(observer); 642 } 643 } 644 }); 645 646 action.run(); 647 648 try { 649 selectCallback.waitForCallback(0); 650 } catch (TimeoutException e) { 651 Assert.fail("Tab selected event was never received"); 652 } 653 instrumentation.runOnMainSync(new Runnable() { 654 @Override 655 public void run() { 656 TabModelSelector selector = activity.getTabModelSelector(); 657 for (TabModel tabModel : selector.getModels()) { 658 tabModel.removeObserver(observer); 659 } 660 } 661 }); 662 } 663 664 /** 665 * Long presses the view, selects an item from the context menu, and 666 * asserts that a new tab is opened and is incognito if expectIncognito is true. 667 * For use in testing long-press context menu options that open new tabs. 668 * 669 * @param testRule The {@link ChromeTabbedActivityTestRule} used to retrieve the currently 670 * running activity. 671 * @param view The {@link View} to long press. 672 * @param contextMenuItemId The context menu item to select on the view. 673 * @param expectIncognito Whether the opened tab is expected to be incognito. 674 * @param expectedUrl The expected url for the new tab. 675 */ 676 public static void invokeContextMenuAndOpenInANewTab(ChromeTabbedActivityTestRule testRule, 677 View view, int contextMenuItemId, boolean expectIncognito, final String expectedUrl) 678 throws ExecutionException { 679 final CallbackHelper createdCallback = new CallbackHelper(); 680 final TabModel tabModel = 681 testRule.getActivity().getTabModelSelector().getModel(expectIncognito); 682 tabModel.addObserver(new TabModelObserver() { 683 @Override 684 public void didAddTab( 685 Tab tab, @TabLaunchType int type, @TabCreationState int creationState) { 686 if (TextUtils.equals(expectedUrl, tab.getUrlString())) { 687 createdCallback.notifyCalled(); 688 tabModel.removeObserver(this); 689 } 690 } 691 }); 692 693 TestTouchUtils.performLongClickOnMainSync( 694 InstrumentationRegistry.getInstrumentation(), view); 695 Assert.assertTrue(InstrumentationRegistry.getInstrumentation().invokeContextMenuAction( 696 testRule.getActivity(), contextMenuItemId, 0)); 697 698 try { 699 createdCallback.waitForCallback(0); 700 } catch (TimeoutException e) { 701 Assert.fail("Never received tab creation event"); 702 } 703 704 if (expectIncognito) { 705 Assert.assertTrue(testRule.getActivity().getTabModelSelector().isIncognitoSelected()); 706 } else { 707 Assert.assertFalse(testRule.getActivity().getTabModelSelector().isIncognitoSelected()); 708 } 709 } 710 711 /** 712 * Long presses the view, selects an item from the context menu, and 713 * asserts that a new tab is opened and is incognito if expectIncognito is true. 714 * For use in testing long-press context menu options that open new tabs in a different 715 * ChromeTabbedActivity instance. 716 * 717 * @param foregroundActivity The {@link ChromeTabbedActivity} currently in the foreground. 718 * @param backgroundActivity The {@link ChromeTabbedActivity} currently in the background. The 719 * new tab is expected to open in this activity. 720 * @param view The {@link View} in the {@code foregroundActivity} to long press. 721 * @param contextMenuItemId The context menu item to select on the view. 722 * @param expectIncognito Whether the opened tab is expected to be incognito. 723 * @param expectedUrl The expected url for the new tab. 724 */ 725 public static void invokeContextMenuAndOpenInOtherWindow( 726 ChromeTabbedActivity foregroundActivity, ChromeTabbedActivity backgroundActivity, 727 View view, int contextMenuItemId, boolean expectIncognito, final String expectedUrl) 728 throws ExecutionException { 729 final CallbackHelper createdCallback = new CallbackHelper(); 730 final TabModel tabModel = 731 backgroundActivity.getTabModelSelector().getModel(expectIncognito); 732 tabModel.addObserver(new TabModelObserver() { 733 @Override 734 public void didAddTab( 735 Tab tab, @TabLaunchType int type, @TabCreationState int creationState) { 736 if (TextUtils.equals(expectedUrl, tab.getUrlString())) { 737 createdCallback.notifyCalled(); 738 tabModel.removeObserver(this); 739 } 740 } 741 }); 742 743 TestTouchUtils.performLongClickOnMainSync( 744 InstrumentationRegistry.getInstrumentation(), view); 745 Assert.assertTrue(InstrumentationRegistry.getInstrumentation().invokeContextMenuAction( 746 foregroundActivity, contextMenuItemId, 0)); 747 748 try { 749 createdCallback.waitForCallback(0); 750 } catch (TimeoutException e) { 751 Assert.fail("Never received tab creation event"); 752 } 753 754 if (expectIncognito) { 755 Assert.assertTrue(backgroundActivity.getTabModelSelector().isIncognitoSelected()); 756 } else { 757 Assert.assertFalse(backgroundActivity.getTabModelSelector().isIncognitoSelected()); 758 } 759 } 760 761 /** 762 * Issues a fake notification about the renderer being killed. 763 * 764 * @param tab {@link Tab} instance where the target renderer resides. 765 * @param wasOomProtected True if the renderer was protected from the OS out-of-memory killer 766 * (e.g. renderer for the currently selected tab) 767 */ 768 public static void simulateRendererKilledForTesting(Tab tab, boolean wasOomProtected) { 769 TabWebContentsObserver observer = TabWebContentsObserver.get(tab); 770 if (observer != null) { 771 observer.simulateRendererKilledForTesting(wasOomProtected); 772 } 773 } 774 775 public static void waitForTitle(Tab tab, String newTitle) { 776 TabTitleObserver titleObserver = new TabTitleObserver(tab, newTitle); 777 try { 778 titleObserver.waitForTitleUpdate(TITLE_UPDATE_TIMEOUT_SECONDS); 779 } catch (TimeoutException e) { 780 Assert.fail(String.format(Locale.ENGLISH, 781 "Tab title didn't update to %s in time.", newTitle)); 782 } 783 } 784 785 /** 786 * @return Whether or not the loading and rendering of the page is done. 787 */ 788 public static boolean isLoadingAndRenderingDone(Tab tab) { 789 return isRendererReady(tab) && tab.getProgress() >= CONSIDERED_READY_LOAD_PERCENTAGE; 790 } 791 792 /** 793 * @return Whether or not the tab has something valid to render. 794 */ 795 public static boolean isRendererReady(Tab tab) { 796 if (tab.getNativePage() != null) return true; 797 WebContents webContents = tab.getWebContents(); 798 if (webContents == null) return false; 799 800 RenderWidgetHostView rwhv = webContents.getRenderWidgetHostView(); 801 return rwhv != null && rwhv.isReady(); 802 } 803 } 804