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