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