1 // Copyright 2017 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;
6 
7 import android.app.Activity;
8 import android.app.Instrumentation;
9 import android.content.ComponentName;
10 import android.content.Intent;
11 import android.net.Uri;
12 import android.os.Bundle;
13 import android.support.test.InstrumentationRegistry;
14 import android.support.test.internal.runner.listener.InstrumentationResultPrinter;
15 import android.support.test.rule.ActivityTestRule;
16 import android.view.Menu;
17 
18 import org.hamcrest.Matchers;
19 import org.junit.Assert;
20 import org.junit.Rule;
21 import org.junit.runner.Description;
22 import org.junit.runners.model.Statement;
23 
24 import org.chromium.base.ActivityState;
25 import org.chromium.base.ApplicationStatus;
26 import org.chromium.base.ApplicationStatus.ActivityStateListener;
27 import org.chromium.base.CommandLine;
28 import org.chromium.base.Log;
29 import org.chromium.base.test.util.CallbackHelper;
30 import org.chromium.base.test.util.Criteria;
31 import org.chromium.base.test.util.CriteriaHelper;
32 import org.chromium.chrome.browser.app.ChromeActivity;
33 import org.chromium.chrome.browser.document.ChromeLauncherActivity;
34 import org.chromium.chrome.browser.flags.ChromeFeatureList;
35 import org.chromium.chrome.browser.infobar.InfoBarContainer;
36 import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
37 import org.chromium.chrome.browser.privacy.settings.PrivacyPreferencesManager;
38 import org.chromium.chrome.browser.tab.Tab;
39 import org.chromium.chrome.browser.tab.TabLaunchType;
40 import org.chromium.chrome.browser.ui.appmenu.AppMenuCoordinator;
41 import org.chromium.chrome.browser.ui.appmenu.AppMenuTestSupport;
42 import org.chromium.chrome.test.util.ChromeApplicationTestUtils;
43 import org.chromium.chrome.test.util.ChromeTabUtils;
44 import org.chromium.chrome.test.util.browser.Features;
45 import org.chromium.components.infobars.InfoBar;
46 import org.chromium.content_public.browser.LoadUrlParams;
47 import org.chromium.content_public.browser.WebContents;
48 import org.chromium.content_public.browser.test.util.JavaScriptUtils;
49 import org.chromium.content_public.browser.test.util.TestThreadUtils;
50 import org.chromium.content_public.common.ContentSwitches;
51 import org.chromium.net.test.EmbeddedTestServer;
52 import org.chromium.net.test.EmbeddedTestServerRule;
53 import org.chromium.ui.KeyboardVisibilityDelegate;
54 import org.chromium.ui.base.PageTransition;
55 
56 import java.util.Calendar;
57 import java.util.List;
58 import java.util.concurrent.Callable;
59 import java.util.concurrent.ExecutionException;
60 import java.util.concurrent.TimeoutException;
61 import java.util.concurrent.atomic.AtomicInteger;
62 import java.util.concurrent.atomic.AtomicReference;
63 
64 /**
65  * Custom  {@link ActivityTestRule} for test using  {@link ChromeActivity}.
66  *
67  * @param <T> The {@link Activity} class under test.
68  */
69 public class ChromeActivityTestRule<T extends ChromeActivity> extends ActivityTestRule<T> {
70     private static final String TAG = "ChromeATR";
71 
72     // The number of ms to wait for the rendering activity to be started.
73     private static final int ACTIVITY_START_TIMEOUT_MS = 1000;
74 
75     private static final long OMNIBOX_FIND_SUGGESTION_TIMEOUT_MS = 10 * 1000;
76 
77     private Thread.UncaughtExceptionHandler mDefaultUncaughtExceptionHandler;
78     private Class<T> mChromeActivityClass;
79     private T mSetActivity;
80     private String mCurrentTestName;
81 
82     @Rule
83     private EmbeddedTestServerRule mTestServerRule = new EmbeddedTestServerRule();
84 
ChromeActivityTestRule(Class<T> activityClass)85     protected ChromeActivityTestRule(Class<T> activityClass) {
86         this(activityClass, false);
87     }
88 
ChromeActivityTestRule(Class<T> activityClass, boolean initialTouchMode)89     protected ChromeActivityTestRule(Class<T> activityClass, boolean initialTouchMode) {
90         super(activityClass, initialTouchMode, false);
91         mChromeActivityClass = activityClass;
92     }
93 
94     @Override
apply(final Statement base, Description description)95     public Statement apply(final Statement base, Description description) {
96         mCurrentTestName = description.getMethodName();
97         Statement chromeActivityStatement = new Statement() {
98             @Override
99             public void evaluate() throws Throwable {
100                 mDefaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
101                 Thread.setDefaultUncaughtExceptionHandler(new ChromeUncaughtExceptionHandler());
102                 ChromeApplicationTestUtils.setUp(InstrumentationRegistry.getTargetContext());
103 
104                 // Preload Calendar so that it does not trigger ReadFromDisk Strict mode violations
105                 // if called on the UI Thread. See https://crbug.com/705477 and
106                 // https://crbug.com/577185
107                 Calendar.getInstance();
108 
109                 // Disable offline indicator UI to prevent it from popping up to obstruct other UI
110                 // views that may make tests flaky.
111                 Features.getInstance().disable(ChromeFeatureList.OFFLINE_INDICATOR);
112                 // Tests are run on bots that are offline by default. This might cause offline UI
113                 // to show and cause flakiness or failures in tests. Using this switch will prevent
114                 // that.
115                 // TODO(crbug.com/1093085): Remove this once we disable the offline indicator for
116                 // specific tests.
117                 CommandLine.getInstance().appendSwitch(
118                         ContentSwitches.FORCE_ONLINE_CONNECTION_STATE_FOR_INDICATOR);
119 
120                 try {
121                     base.evaluate();
122                 } finally {
123                     Thread.setDefaultUncaughtExceptionHandler(mDefaultUncaughtExceptionHandler);
124                 }
125             }
126         };
127         Statement testServerStatement = mTestServerRule.apply(chromeActivityStatement, description);
128         return super.apply(testServerStatement, description);
129     }
130 
131     /**
132      * Return the timeout limit for Chrome activty start in tests
133      */
getActivityStartTimeoutMs()134     public static int getActivityStartTimeoutMs() {
135         return ACTIVITY_START_TIMEOUT_MS;
136     }
137 
138     // TODO(yolandyan): remove this once startActivityCompletely is refactored out of
139     // ChromeActivityTestRule
140     @Override
getActivity()141     public T getActivity() {
142         if (mSetActivity != null) {
143             return mSetActivity;
144         }
145         return super.getActivity();
146     }
147 
148     /** Retrieves the application Menu */
getMenu()149     public Menu getMenu() throws ExecutionException {
150         return TestThreadUtils.runOnUiThreadBlocking(
151                 () -> AppMenuTestSupport.getMenu(getAppMenuCoordinator()));
152     }
153 
154     /**
155      * @return The {@link AppMenuCoordinator} for the activity.
156      */
getAppMenuCoordinator()157     public AppMenuCoordinator getAppMenuCoordinator() {
158         return getActivity().getRootUiCoordinatorForTesting().getAppMenuCoordinatorForTesting();
159     }
160 
161     /**
162      * Matches testString against baseString.
163      * Returns 0 if there is no match, 1 if an exact match and 2 if a fuzzy match.
164      */
matchUrl(String baseString, String testString)165     public static int matchUrl(String baseString, String testString) {
166         if (baseString.equals(testString)) {
167             return 1;
168         }
169         if (baseString.contains(testString)) {
170             return 2;
171         }
172         return 0;
173     }
174 
175     /**
176      * Waits for the activity to fully finish its native initialization.
177      * @param activity The {@link ChromeActivity} to wait for.
178      */
waitForActivityNativeInitializationComplete(ChromeActivity activity)179     public static void waitForActivityNativeInitializationComplete(ChromeActivity activity) {
180         CriteriaHelper.pollUiThread(
181                 ()
182                         -> ChromeBrowserInitializer.getInstance().isFullBrowserInitialized(),
183                 "Native initialization never finished",
184                 20 * CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL,
185                 CriteriaHelper.DEFAULT_POLLING_INTERVAL);
186 
187         CriteriaHelper.pollUiThread(() -> activity.didFinishNativeInitialization(),
188                 "Native initialization (of Activity) never finished");
189     }
190 
191     /**
192      * Waits for the activity to fully finish its native initialization.
193      */
waitForActivityNativeInitializationComplete()194     public void waitForActivityNativeInitializationComplete() {
195         waitForActivityNativeInitializationComplete(getActivity());
196     }
197 
198     /**
199      * Invokes {@link Instrumentation#startActivitySync(Intent)} and sets the
200      * test case's activity to the result. See the documentation for
201      * {@link Instrumentation#startActivitySync(Intent)} on the timing of the
202      * return, but generally speaking the activity's "onCreate" has completed
203      * and the activity's main looper has become idle.
204      *
205      * TODO(yolandyan): very similar to ActivityTestRule#launchActivity(Intent),
206      * yet small differences remains (e.g. launchActivity() uses FLAG_ACTIVITY_NEW_TASK while
207      * startActivityCompletely doesn't), need to refactor and use only launchActivity
208      * after the JUnit4 migration
209      */
startActivityCompletely(Intent intent)210     public void startActivityCompletely(Intent intent) {
211         Features.ensureCommandLineIsUpToDate();
212 
213         final CallbackHelper activityCallback = new CallbackHelper();
214         final AtomicReference<T> activityRef = new AtomicReference<>();
215         ActivityStateListener stateListener = new ActivityStateListener() {
216             @SuppressWarnings("unchecked")
217             @Override
218             public void onActivityStateChange(Activity activity, int newState) {
219                 if (newState == ActivityState.RESUMED) {
220                     if (!mChromeActivityClass.isAssignableFrom(activity.getClass())) return;
221 
222                     activityRef.set((T) activity);
223                     activityCallback.notifyCalled();
224                     ApplicationStatus.unregisterActivityStateListener(this);
225                 }
226             }
227         };
228         ApplicationStatus.registerStateListenerForAllActivities(stateListener);
229 
230         try {
231             InstrumentationRegistry.getInstrumentation().startActivitySync(intent);
232             activityCallback.waitForCallback("Activity did not start as expected", 0);
233             T activity = activityRef.get();
234             Assert.assertNotNull("Activity reference is null.", activity);
235             setActivity(activity);
236             Log.d(TAG, "startActivityCompletely <<");
237         } catch (TimeoutException e) {
238             throw new RuntimeException(e);
239         } finally {
240             ApplicationStatus.unregisterActivityStateListener(stateListener);
241         }
242     }
243 
244     /**
245      * Enables or disables network predictions, i.e. prerendering, prefetching, DNS preresolution,
246      * etc. Network predictions are enabled by default.
247      */
setNetworkPredictionEnabled(final boolean enabled)248     public void setNetworkPredictionEnabled(final boolean enabled) {
249         InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
250             @Override
251             public void run() {
252                 PrivacyPreferencesManager.getInstance().setNetworkPredictionEnabled(enabled);
253             }
254         });
255     }
256 
257     /**
258      * Navigates to a URL directly without going through the UrlBar. This bypasses the page
259      * preloading mechanism of the UrlBar.
260      * @param url            The URL to load in the current tab.
261      * @param secondsToWait  The number of seconds to wait for the page to be loaded.
262      * @return FULL_PRERENDERED_PAGE_LOAD or PARTIAL_PRERENDERED_PAGE_LOAD if the page has been
263      *         prerendered. DEFAULT_PAGE_LOAD if it had not.
264      */
loadUrl(String url, long secondsToWait)265     public int loadUrl(String url, long secondsToWait) throws IllegalArgumentException {
266         return loadUrlInTab(url, PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR,
267                 getActivity().getActivityTab(), secondsToWait);
268     }
269 
270     /**
271      * Navigates to a URL directly without going through the UrlBar. This bypasses the page
272      * preloading mechanism of the UrlBar.
273      * @param url The URL to load in the current tab.
274      * @return FULL_PRERENDERED_PAGE_LOAD or PARTIAL_PRERENDERED_PAGE_LOAD if the page has been
275      *         prerendered. DEFAULT_PAGE_LOAD if it had not.
276      */
loadUrl(String url)277     public int loadUrl(String url) throws IllegalArgumentException {
278         return loadUrlInTab(url, PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR,
279                 getActivity().getActivityTab());
280     }
281 
282     /**
283      * @param url            The URL of the page to load.
284      * @param pageTransition The type of transition. see
285      *                       {@link org.chromium.ui.base.PageTransition}
286      *                       for valid values.
287      * @param tab            The tab to load the URL into.
288      * @param secondsToWait  The number of seconds to wait for the page to be loaded.
289      * @return               FULL_PRERENDERED_PAGE_LOAD or PARTIAL_PRERENDERED_PAGE_LOAD if the
290      *                       page has been prerendered. DEFAULT_PAGE_LOAD if it had not.
291      */
loadUrlInTab(String url, int pageTransition, Tab tab, long secondsToWait)292     public int loadUrlInTab(String url, int pageTransition, Tab tab, long secondsToWait) {
293         Assert.assertNotNull("Cannot load the URL in a null tab", tab);
294         final AtomicInteger result = new AtomicInteger();
295 
296         ChromeTabUtils.waitForTabPageLoaded(tab, url, new Runnable() {
297             @Override
298             public void run() {
299                 TestThreadUtils.runOnUiThreadBlocking(
300                         () -> { result.set(tab.loadUrl(new LoadUrlParams(url, pageTransition))); });
301             }
302         }, secondsToWait);
303         ChromeTabUtils.waitForInteractable(tab);
304         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
305         return result.get();
306     }
307 
308     /**
309      * @param url            The URL of the page to load.
310      * @param pageTransition The type of transition. see
311      *                       {@link org.chromium.ui.base.PageTransition}
312      *                       for valid values.
313      * @param tab            The tab to load the URL into.
314      * @return               FULL_PRERENDERED_PAGE_LOAD or PARTIAL_PRERENDERED_PAGE_LOAD if the
315      *                       page has been prerendered. DEFAULT_PAGE_LOAD if it had not.
316      */
loadUrlInTab(String url, int pageTransition, Tab tab)317     public int loadUrlInTab(String url, int pageTransition, Tab tab) {
318         return loadUrlInTab(url, pageTransition, tab, CallbackHelper.WAIT_TIMEOUT_SECONDS);
319     }
320 
321     /**
322      * Load a URL in a new tab. The {@link Tab} will pretend to be created from a link.
323      * @param url The URL of the page to load.
324      */
loadUrlInNewTab(String url)325     public Tab loadUrlInNewTab(String url) {
326         return loadUrlInNewTab(url, false);
327     }
328 
329     /**
330      * Load a URL in a new tab. The {@link Tab} will pretend to be created from a link.
331      * @param url The URL of the page to load.
332      * @param incognito Whether the new tab should be incognito.
333      */
loadUrlInNewTab(final String url, final boolean incognito)334     public Tab loadUrlInNewTab(final String url, final boolean incognito) {
335         return loadUrlInNewTab(url, incognito, TabLaunchType.FROM_LINK);
336     }
337 
338     /**
339      * Load a URL in a new tab, with the given transition type.
340      * @param url The URL of the page to load.
341      * @param incognito Whether the new tab should be incognito.
342      * @param launchType The type of Tab Launch.
343      */
loadUrlInNewTab( final String url, final boolean incognito, final @TabLaunchType int launchType)344     public Tab loadUrlInNewTab(
345             final String url, final boolean incognito, final @TabLaunchType int launchType) {
346         Tab tab = null;
347         try {
348             tab = TestThreadUtils.runOnUiThreadBlocking(new Callable<Tab>() {
349                 @Override
350                 public Tab call() {
351                     return getActivity().getTabCreator(incognito).launchUrl(url, launchType);
352                 }
353             });
354         } catch (ExecutionException e) {
355             Assert.fail("Failed to create new tab");
356         }
357         ChromeTabUtils.waitForTabPageLoaded(tab, url);
358         ChromeTabUtils.waitForInteractable(tab);
359         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
360         return tab;
361     }
362 
363     /**
364      * Prepares a URL intent to start the activity.
365      * @param intent the intent to be modified
366      * @param url the URL to be used (may be null)
367      */
prepareUrlIntent(Intent intent, String url)368     public Intent prepareUrlIntent(Intent intent, String url) {
369         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
370         if (intent.getComponent() == null) {
371             intent.setComponent(new ComponentName(
372                     InstrumentationRegistry.getTargetContext(), ChromeLauncherActivity.class));
373         }
374 
375         if (url != null) {
376             intent.setData(Uri.parse(url));
377         }
378         return intent;
379     }
380 
381     /**
382      * @return The number of tabs currently open.
383      */
tabsCount(boolean incognito)384     public int tabsCount(boolean incognito) {
385         return TestThreadUtils.runOnUiThreadBlockingNoException(new Callable<Integer>() {
386             @Override
387             public Integer call() {
388                 return getActivity().getTabModelSelector().getModel(incognito).getCount();
389             }
390         });
391     }
392 
393     /**
394      * Returns the infobars being displayed by the current tab, or null if they don't exist.
395      */
396     public List<InfoBar> getInfoBars() {
397         return TestThreadUtils.runOnUiThreadBlockingNoException(new Callable<List<InfoBar>>() {
398             @Override
399             public List<InfoBar> call() {
400                 Tab currentTab = getActivity().getActivityTab();
401                 Assert.assertNotNull(currentTab);
402                 Assert.assertNotNull(InfoBarContainer.get(currentTab));
403                 return InfoBarContainer.get(currentTab).getInfoBarsForTesting();
404             }
405         });
406     }
407 
408     /**
409      * Executes the given snippet of JavaScript code within the current tab. Returns the result of
410      * its execution in JSON format.
411      */
412     public String runJavaScriptCodeInCurrentTab(String code) throws TimeoutException {
413         return JavaScriptUtils.executeJavaScriptAndWaitForResult(
414                 getActivity().getCurrentWebContents(), code);
415     }
416 
417     /**
418      * Waits till the WebContents receives the expected page scale factor
419      * from the compositor and asserts that this happens.
420      */
421     public void assertWaitForPageScaleFactorMatch(float expectedScale) {
422         ChromeApplicationTestUtils.assertWaitForPageScaleFactorMatch(getActivity(), expectedScale);
423     }
424 
425     public String getName() {
426         return mCurrentTestName;
427     }
428 
429     public String getTestName() {
430         return mCurrentTestName;
431     }
432 
433     /**
434      * @return {@link InfoBarContainer} of the active tab of the activity.
435      *     {@code null} if there is no tab for the activity or infobar is available.
436      */
437     public InfoBarContainer getInfoBarContainer() {
438         return TestThreadUtils.runOnUiThreadBlockingNoException(
439                 () -> getActivity().getActivityTab() != null
440                         ? InfoBarContainer.get(getActivity().getActivityTab())
441                         : null);
442     }
443 
444     /**
445      * Gets the ChromeActivityTestRule's EmbeddedTestServer instance if it has one.
446      */
447     public EmbeddedTestServer getTestServer() {
448         return mTestServerRule.getServer();
449     }
450 
451     /**
452      * Gets the underlying EmbeddedTestServerRule for getTestServer().
453      */
454     public EmbeddedTestServerRule getEmbeddedTestServerRule() {
455         return mTestServerRule;
456     }
457 
458     /**
459      * @return {@link WebContents} of the active tab of the activity.
460      */
461     public WebContents getWebContents() {
462         return getActivity().getActivityTab().getWebContents();
463     }
464 
465     /**
466      * @return {@link KeyboardVisibilityDelegate} for the activity.
467      */
468     public KeyboardVisibilityDelegate getKeyboardDelegate() {
469         if (getActivity().getWindowAndroid() == null) {
470             return KeyboardVisibilityDelegate.getInstance();
471         }
472         return getActivity().getWindowAndroid().getKeyboardDelegate();
473     }
474 
475     public void setActivity(T chromeActivity) {
476         mSetActivity = chromeActivity;
477     }
478 
479     /**
480      * Waits for an Activity of the given class to be started.
481      * @return The Activity.
482      */
483     @SuppressWarnings("unchecked")
484     public static <T extends ChromeActivity> T waitFor(final Class<T> expectedClass) {
485         final Activity[] holder = new Activity[1];
486         CriteriaHelper.pollUiThread(() -> {
487             holder[0] = ApplicationStatus.getLastTrackedFocusedActivity();
488             Criteria.checkThat(holder[0], Matchers.notNullValue());
489             Criteria.checkThat(holder[0].getClass(), Matchers.typeCompatibleWith(expectedClass));
490             Criteria.checkThat(
491                     ((ChromeActivity) holder[0]).getActivityTab(), Matchers.notNullValue());
492         });
493         return (T) holder[0];
494     }
495 
496     private class ChromeUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
497         @Override
498         public void uncaughtException(Thread t, Throwable e) {
499             String stackTrace = android.util.Log.getStackTraceString(e);
500             if (e.getClass().getName().endsWith("StrictModeViolation")) {
501                 stackTrace += "\nSearch logcat for \"StrictMode policy violation\" for full stack.";
502             }
503             Bundle resultsBundle = new Bundle();
504             resultsBundle.putString(
505                     InstrumentationResultPrinter.REPORT_KEY_NAME_CLASS, getClass().getName());
506             resultsBundle.putString(
507                     InstrumentationResultPrinter.REPORT_KEY_NAME_TEST, mCurrentTestName);
508             resultsBundle.putString(InstrumentationResultPrinter.REPORT_KEY_STACK, stackTrace);
509             InstrumentationRegistry.getInstrumentation().sendStatus(-1, resultsBundle);
510             mDefaultUncaughtExceptionHandler.uncaughtException(t, e);
511         }
512     }
513 }
514