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.android_webview.test; 6 7 import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout; 8 9 import android.content.Context; 10 import android.support.test.InstrumentationRegistry; 11 import android.support.test.rule.ActivityTestRule; 12 import android.util.AndroidRuntimeException; 13 import android.util.Base64; 14 import android.view.ViewGroup; 15 16 import org.junit.Assert; 17 import org.junit.runner.Description; 18 import org.junit.runners.model.Statement; 19 20 import org.chromium.android_webview.AwBrowserContext; 21 import org.chromium.android_webview.AwBrowserProcess; 22 import org.chromium.android_webview.AwContents; 23 import org.chromium.android_webview.AwContents.DependencyFactory; 24 import org.chromium.android_webview.AwContents.InternalAccessDelegate; 25 import org.chromium.android_webview.AwContents.NativeDrawFunctorFactory; 26 import org.chromium.android_webview.AwContentsClient; 27 import org.chromium.android_webview.AwSettings; 28 import org.chromium.android_webview.test.util.GraphicsTestUtils; 29 import org.chromium.android_webview.test.util.JSUtils; 30 import org.chromium.base.Log; 31 import org.chromium.base.test.util.CallbackHelper; 32 import org.chromium.base.test.util.CriteriaHelper; 33 import org.chromium.base.test.util.InMemorySharedPreferences; 34 import org.chromium.content_public.browser.LoadUrlParams; 35 import org.chromium.content_public.browser.test.util.TestCallbackHelperContainer.OnPageFinishedHelper; 36 import org.chromium.content_public.browser.test.util.TestThreadUtils; 37 import org.chromium.net.test.util.TestWebServer; 38 39 import java.lang.annotation.Annotation; 40 import java.lang.ref.WeakReference; 41 import java.util.ArrayList; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.concurrent.BlockingQueue; 45 import java.util.concurrent.Callable; 46 import java.util.concurrent.ExecutionException; 47 import java.util.concurrent.Future; 48 import java.util.concurrent.TimeUnit; 49 import java.util.concurrent.TimeoutException; 50 import java.util.regex.Matcher; 51 import java.util.regex.Pattern; 52 53 /** Custom ActivityTestRunner for WebView instrumentation tests */ 54 public class AwActivityTestRule extends ActivityTestRule<AwTestRunnerActivity> { 55 public static final long WAIT_TIMEOUT_MS = scaleTimeout(15000L); 56 57 public static final int CHECK_INTERVAL = 100; 58 59 private static final String TAG = "AwActivityTestRule"; 60 61 private static final Pattern MAYBE_QUOTED_STRING = Pattern.compile("^(\"?)(.*)\\1$"); 62 63 private static boolean sBrowserProcessStarted; 64 65 /** 66 * An interface to call onCreateWindow(AwContents). 67 */ 68 public interface OnCreateWindowHandler { 69 /** This will be called when a new window pops up from the current webview. */ onCreateWindow(AwContents awContents)70 public boolean onCreateWindow(AwContents awContents); 71 } 72 73 private Description mCurrentTestDescription; 74 75 // The browser context needs to be a process-wide singleton. 76 private AwBrowserContext mBrowserContext; 77 78 private List<WeakReference<AwContents>> mAwContentsDestroyedInTearDown = new ArrayList<>(); 79 AwActivityTestRule()80 public AwActivityTestRule() { 81 super(AwTestRunnerActivity.class, /* initialTouchMode */ false, /* launchActivity */ false); 82 } 83 84 @Override apply(final Statement base, Description description)85 public Statement apply(final Statement base, Description description) { 86 mCurrentTestDescription = description; 87 return super.apply(new Statement() { 88 @Override 89 public void evaluate() throws Throwable { 90 setUp(); 91 base.evaluate(); 92 tearDown(); 93 } 94 }, description); 95 } 96 97 public void setUp() { 98 if (needsAwBrowserContextCreated()) { 99 createAwBrowserContext(); 100 } 101 if (needsBrowserProcessStarted()) { 102 startBrowserProcess(); 103 } else { 104 assert !sBrowserProcessStarted 105 : "needsBrowserProcessStarted false and @Batch are incompatible"; 106 } 107 } 108 109 public void tearDown() { 110 if (!needsAwContentsCleanup()) return; 111 112 TestThreadUtils.runOnUiThreadBlocking(() -> { 113 for (WeakReference<AwContents> awContentsRef : mAwContentsDestroyedInTearDown) { 114 AwContents awContents = awContentsRef.get(); 115 if (awContents == null) continue; 116 awContents.destroy(); 117 } 118 }); 119 // Flush the UI queue since destroy posts again to UI thread. 120 TestThreadUtils.runOnUiThreadBlocking(() -> { mAwContentsDestroyedInTearDown.clear(); }); 121 } 122 123 public AwTestRunnerActivity launchActivity() { 124 if (getActivity() == null) { 125 return launchActivity(null); 126 } 127 return getActivity(); 128 } 129 130 public AwBrowserContext createAwBrowserContextOnUiThread(InMemorySharedPreferences prefs) { 131 // Native pointer is initialized later in startBrowserProcess if needed. 132 return new AwBrowserContext(prefs, 0, true); 133 } 134 135 public TestDependencyFactory createTestDependencyFactory() { 136 return new TestDependencyFactory(); 137 } 138 139 /** 140 * Override this to return false if the test doesn't want to create an 141 * AwBrowserContext automatically. 142 */ 143 public boolean needsAwBrowserContextCreated() { 144 return true; 145 } 146 147 /** 148 * Override this to return false if the test doesn't want the browser 149 * startup sequence to be run automatically. 150 * 151 * @return Whether the instrumentation test requires the browser process to 152 * already be started. 153 */ 154 public boolean needsBrowserProcessStarted() { 155 return true; 156 } 157 158 /** 159 * Override this to return false if test doesn't need all AwContents to be 160 * destroyed explicitly after the test. 161 */ 162 public boolean needsAwContentsCleanup() { 163 return true; 164 } 165 166 public void createAwBrowserContext() { 167 if (mBrowserContext != null) { 168 throw new AndroidRuntimeException("There should only be one browser context."); 169 } 170 launchActivity(); // The Activity must be launched in order to load native code 171 final InMemorySharedPreferences prefs = new InMemorySharedPreferences(); 172 TestThreadUtils.runOnUiThreadBlockingNoException( 173 () -> mBrowserContext = createAwBrowserContextOnUiThread(prefs)); 174 } 175 176 public void startBrowserProcess() { 177 // The Activity must be launched in order for proper webview statics to be setup. 178 launchActivity(); 179 if (!sBrowserProcessStarted) { 180 sBrowserProcessStarted = true; 181 TestThreadUtils.runOnUiThreadBlocking(() -> AwBrowserProcess.start()); 182 } 183 if (mBrowserContext != null) { 184 TestThreadUtils.runOnUiThreadBlocking( 185 () -> mBrowserContext.setNativePointer( 186 AwBrowserContext.getDefault().getNativePointer())); 187 } 188 } 189 190 public static void enableJavaScriptOnUiThread(final AwContents awContents) { 191 TestThreadUtils.runOnUiThreadBlocking( 192 () -> awContents.getSettings().setJavaScriptEnabled(true)); 193 } 194 195 public static void setNetworkAvailableOnUiThread( 196 final AwContents awContents, final boolean networkUp) { 197 TestThreadUtils.runOnUiThreadBlocking(() -> awContents.setNetworkAvailable(networkUp)); 198 } 199 200 /** 201 * Loads url on the UI thread and blocks until onPageFinished is called. 202 */ 203 public void loadUrlSync(final AwContents awContents, CallbackHelper onPageFinishedHelper, 204 final String url) throws Exception { 205 loadUrlSync(awContents, onPageFinishedHelper, url, null); 206 } 207 208 public void loadUrlSync(final AwContents awContents, CallbackHelper onPageFinishedHelper, 209 final String url, final Map<String, String> extraHeaders) throws Exception { 210 int currentCallCount = onPageFinishedHelper.getCallCount(); 211 loadUrlAsync(awContents, url, extraHeaders); 212 onPageFinishedHelper.waitForCallback( 213 currentCallCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 214 } 215 216 public void loadUrlSyncAndExpectError(final AwContents awContents, 217 CallbackHelper onPageFinishedHelper, CallbackHelper onReceivedErrorHelper, 218 final String url) throws Exception { 219 int onErrorCallCount = onReceivedErrorHelper.getCallCount(); 220 int onFinishedCallCount = onPageFinishedHelper.getCallCount(); 221 loadUrlAsync(awContents, url); 222 onReceivedErrorHelper.waitForCallback( 223 onErrorCallCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 224 onPageFinishedHelper.waitForCallback( 225 onFinishedCallCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 226 } 227 228 /** 229 * Loads url on the UI thread but does not block. 230 */ 231 public void loadUrlAsync(final AwContents awContents, final String url) { 232 loadUrlAsync(awContents, url, null); 233 } 234 235 public void loadUrlAsync( 236 final AwContents awContents, final String url, final Map<String, String> extraHeaders) { 237 TestThreadUtils.runOnUiThreadBlocking(() -> awContents.loadUrl(url, extraHeaders)); 238 } 239 240 /** 241 * Posts url on the UI thread and blocks until onPageFinished is called. 242 */ 243 public void postUrlSync(final AwContents awContents, CallbackHelper onPageFinishedHelper, 244 final String url, byte[] postData) throws Exception { 245 int currentCallCount = onPageFinishedHelper.getCallCount(); 246 postUrlAsync(awContents, url, postData); 247 onPageFinishedHelper.waitForCallback( 248 currentCallCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 249 } 250 251 /** 252 * Loads url on the UI thread but does not block. 253 */ 254 public void postUrlAsync(final AwContents awContents, final String url, byte[] postData) { 255 class PostUrl implements Runnable { 256 byte[] mPostData; 257 public PostUrl(byte[] postData) { 258 mPostData = postData; 259 } 260 @Override 261 public void run() { 262 awContents.postUrl(url, mPostData); 263 } 264 } 265 TestThreadUtils.runOnUiThreadBlocking(new PostUrl(postData)); 266 } 267 268 /** 269 * Loads data on the UI thread and blocks until onPageFinished is called. 270 */ 271 public void loadDataSync(final AwContents awContents, CallbackHelper onPageFinishedHelper, 272 final String data, final String mimeType, final boolean isBase64Encoded) 273 throws Exception { 274 int currentCallCount = onPageFinishedHelper.getCallCount(); 275 loadDataAsync(awContents, data, mimeType, isBase64Encoded); 276 onPageFinishedHelper.waitForCallback( 277 currentCallCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 278 } 279 280 public void loadHtmlSync(final AwContents awContents, CallbackHelper onPageFinishedHelper, 281 final String html) throws Throwable { 282 int currentCallCount = onPageFinishedHelper.getCallCount(); 283 final String encodedData = Base64.encodeToString(html.getBytes(), Base64.NO_PADDING); 284 loadDataSync(awContents, onPageFinishedHelper, encodedData, "text/html", true); 285 } 286 287 public void loadDataSyncWithCharset(final AwContents awContents, 288 CallbackHelper onPageFinishedHelper, final String data, final String mimeType, 289 final boolean isBase64Encoded, final String charset) throws Exception { 290 int currentCallCount = onPageFinishedHelper.getCallCount(); 291 TestThreadUtils.runOnUiThreadBlocking( 292 () -> awContents.loadUrl(LoadUrlParams.createLoadDataParams( 293 data, mimeType, isBase64Encoded, charset))); 294 onPageFinishedHelper.waitForCallback( 295 currentCallCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 296 } 297 298 /** 299 * Loads data on the UI thread but does not block. 300 */ 301 public void loadDataAsync(final AwContents awContents, final String data, final String mimeType, 302 final boolean isBase64Encoded) { 303 TestThreadUtils.runOnUiThreadBlocking( 304 () -> awContents.loadData(data, mimeType, isBase64Encoded ? "base64" : null)); 305 } 306 307 public void loadDataWithBaseUrlSync(final AwContents awContents, 308 CallbackHelper onPageFinishedHelper, final String data, final String mimeType, 309 final boolean isBase64Encoded, final String baseUrl, final String historyUrl) 310 throws Throwable { 311 int currentCallCount = onPageFinishedHelper.getCallCount(); 312 loadDataWithBaseUrlAsync(awContents, data, mimeType, isBase64Encoded, baseUrl, historyUrl); 313 onPageFinishedHelper.waitForCallback( 314 currentCallCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 315 } 316 317 public void loadDataWithBaseUrlAsync(final AwContents awContents, final String data, 318 final String mimeType, final boolean isBase64Encoded, final String baseUrl, 319 final String historyUrl) throws Throwable { 320 runOnUiThread(() -> awContents.loadDataWithBaseURL(baseUrl, data, mimeType, 321 isBase64Encoded ? "base64" : null, historyUrl)); 322 } 323 324 /** 325 * Reloads the current page synchronously. 326 */ 327 public void reloadSync(final AwContents awContents, CallbackHelper onPageFinishedHelper) 328 throws Exception { 329 int currentCallCount = onPageFinishedHelper.getCallCount(); 330 TestThreadUtils.runOnUiThreadBlocking( 331 () -> awContents.getNavigationController().reload(true)); 332 onPageFinishedHelper.waitForCallback( 333 currentCallCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 334 } 335 336 /** 337 * Stops loading on the UI thread. 338 */ 339 public void stopLoading(final AwContents awContents) { 340 TestThreadUtils.runOnUiThreadBlocking(() -> awContents.stopLoading()); 341 } 342 343 public void waitForVisualStateCallback(final AwContents awContents) throws Exception { 344 final CallbackHelper ch = new CallbackHelper(); 345 final int chCount = ch.getCallCount(); 346 TestThreadUtils.runOnUiThreadBlocking(() -> { 347 final long requestId = 666; 348 awContents.insertVisualStateCallback(requestId, new AwContents.VisualStateCallback() { 349 @Override 350 public void onComplete(long id) { 351 Assert.assertEquals(requestId, id); 352 ch.notifyCalled(); 353 } 354 }); 355 }); 356 ch.waitForCallback(chCount); 357 } 358 359 public void insertVisualStateCallbackOnUIThread(final AwContents awContents, 360 final long requestId, final AwContents.VisualStateCallback callback) { 361 TestThreadUtils.runOnUiThreadBlocking( 362 () -> awContents.insertVisualStateCallback(requestId, callback)); 363 } 364 365 // Waits for the pixel at the center of AwContents to color up into expectedColor. 366 // Note that this is a stricter condition that waiting for a visual state callback, 367 // as visual state callback only indicates that *something* has appeared in WebView. 368 public void waitForPixelColorAtCenterOfView(final AwContents awContents, 369 final AwTestContainerView testContainerView, final int expectedColor) { 370 pollUiThread(() -> GraphicsTestUtils.getPixelColorAtCenterOfView( 371 awContents, testContainerView) == expectedColor); 372 } 373 374 public AwTestContainerView createAwTestContainerView(final AwContentsClient awContentsClient) { 375 return createAwTestContainerView(awContentsClient, false, null); 376 } 377 378 public AwTestContainerView createAwTestContainerView(final AwContentsClient awContentsClient, 379 boolean supportsLegacyQuirks, final TestDependencyFactory testDependencyFactory) { 380 AwTestContainerView testContainerView = createDetachedAwTestContainerView( 381 awContentsClient, supportsLegacyQuirks, testDependencyFactory); 382 getActivity().addView(testContainerView); 383 testContainerView.requestFocus(); 384 return testContainerView; 385 } 386 387 public AwBrowserContext getAwBrowserContext() { 388 return mBrowserContext; 389 } 390 391 public AwTestContainerView createDetachedAwTestContainerView( 392 final AwContentsClient awContentsClient) { 393 return createDetachedAwTestContainerView(awContentsClient, false, null); 394 } 395 396 public AwTestContainerView createDetachedAwTestContainerView( 397 final AwContentsClient awContentsClient, boolean supportsLegacyQuirks, 398 TestDependencyFactory testDependencyFactory) { 399 if (testDependencyFactory == null) { 400 testDependencyFactory = createTestDependencyFactory(); 401 } 402 boolean allowHardwareAcceleration = isHardwareAcceleratedTest(); 403 final AwTestContainerView testContainerView = 404 testDependencyFactory.createAwTestContainerView( 405 getActivity(), allowHardwareAcceleration); 406 407 AwSettings awSettings = 408 testDependencyFactory.createAwSettings(getActivity(), supportsLegacyQuirks); 409 AwContents awContents = testDependencyFactory.createAwContents(mBrowserContext, 410 testContainerView, testContainerView.getContext(), 411 testContainerView.getInternalAccessDelegate(), 412 testContainerView.getNativeDrawFunctorFactory(), awContentsClient, awSettings, 413 testDependencyFactory); 414 testContainerView.initialize(awContents); 415 mAwContentsDestroyedInTearDown.add(new WeakReference<>(awContents)); 416 return testContainerView; 417 } 418 419 public boolean isHardwareAcceleratedTest() { 420 return !testMethodHasAnnotation(DisableHardwareAccelerationForTest.class); 421 } 422 423 public AwTestContainerView createAwTestContainerViewOnMainSync(final AwContentsClient client) { 424 return createAwTestContainerViewOnMainSync(client, false, null); 425 } 426 427 public AwTestContainerView createAwTestContainerViewOnMainSync( 428 final AwContentsClient client, final boolean supportsLegacyQuirks) { 429 return createAwTestContainerViewOnMainSync(client, supportsLegacyQuirks, null); 430 } 431 432 public AwTestContainerView createAwTestContainerViewOnMainSync(final AwContentsClient client, 433 final boolean supportsLegacyQuirks, final TestDependencyFactory testDependencyFactory) { 434 return TestThreadUtils.runOnUiThreadBlockingNoException( 435 () -> createAwTestContainerView( 436 client, supportsLegacyQuirks, testDependencyFactory)); 437 } 438 439 public void destroyAwContentsOnMainSync(final AwContents awContents) { 440 if (awContents == null) return; 441 TestThreadUtils.runOnUiThreadBlocking(() -> awContents.destroy()); 442 } 443 444 public String getTitleOnUiThread(final AwContents awContents) throws Exception { 445 return TestThreadUtils.runOnUiThreadBlocking(() -> awContents.getTitle()); 446 } 447 448 public AwSettings getAwSettingsOnUiThread(final AwContents awContents) throws Exception { 449 return TestThreadUtils.runOnUiThreadBlocking(() -> awContents.getSettings()); 450 } 451 452 /** 453 * Verify double quotes in both sides of the raw string. Strip the double quotes and 454 * returns rest of the string. 455 */ 456 public String maybeStripDoubleQuotes(String raw) { 457 Assert.assertNotNull(raw); 458 Matcher m = MAYBE_QUOTED_STRING.matcher(raw); 459 Assert.assertTrue(m.matches()); 460 return m.group(2); 461 } 462 463 /** 464 * Executes the given snippet of JavaScript code within the given ContentView. Returns the 465 * result of its execution in JSON format. 466 */ 467 public String executeJavaScriptAndWaitForResult(final AwContents awContents, 468 TestAwContentsClient viewClient, final String code) throws Exception { 469 return JSUtils.executeJavaScriptAndWaitForResult( 470 InstrumentationRegistry.getInstrumentation(), awContents, 471 viewClient.getOnEvaluateJavaScriptResultHelper(), code); 472 } 473 474 /** 475 * Executes JavaScript code within the given ContentView to get the text content in 476 * document body. Returns the result string without double quotes. 477 */ 478 public String getJavaScriptResultBodyTextContent( 479 final AwContents awContents, final TestAwContentsClient viewClient) throws Exception { 480 String raw = executeJavaScriptAndWaitForResult( 481 awContents, viewClient, "document.body.textContent"); 482 return maybeStripDoubleQuotes(raw); 483 } 484 485 /** 486 * Adds a JavaScript interface to the AwContents. Does its work synchronously on the UI thread, 487 * and can be called from any thread. All the rules of {@link 488 * android.webkit.WebView#addJavascriptInterface} apply to this method (ex. you must call this 489 * <b>prior</b> to loading the frame you intend to load the JavaScript interface into). 490 * 491 * @param awContents the AwContents in which to insert the JavaScript interface. 492 * @param objectToInject the JavaScript interface to inject. 493 * @param javascriptIdentifier the name with which to refer to {@code objectToInject} from 494 * JavaScript code. 495 */ 496 public static void addJavascriptInterfaceOnUiThread(final AwContents awContents, 497 final Object objectToInject, final String javascriptIdentifier) { 498 TestThreadUtils.runOnUiThreadBlocking( 499 () -> awContents.addJavascriptInterface(objectToInject, javascriptIdentifier)); 500 } 501 502 /** 503 * Wrapper around CriteriaHelper.pollInstrumentationThread. This uses AwActivityTestRule-specifc 504 * timeouts and treats timeouts and exceptions as test failures automatically. 505 */ 506 public static void pollInstrumentationThread(final Callable<Boolean> callable) { 507 CriteriaHelper.pollInstrumentationThread(() -> { 508 try { 509 return callable.call(); 510 } catch (Throwable e) { 511 Log.e(TAG, "Exception while polling.", e); 512 return false; 513 } 514 }, WAIT_TIMEOUT_MS, CHECK_INTERVAL); 515 } 516 517 /** 518 * Wrapper around {@link AwActivityTestRule#pollInstrumentationThread()} but runs the 519 * callable on the UI thread. 520 */ 521 public void pollUiThread(final Callable<Boolean> callable) { 522 pollInstrumentationThread(() -> TestThreadUtils.runOnUiThreadBlocking(callable)); 523 } 524 525 /** 526 * Waits for {@code future} and returns its value (or times out). If {@code future} has an 527 * associated Exception, this will re-throw that Exception on the instrumentation thread 528 * (wrapping with an unchecked Exception if necessary, to avoid requiring callers to declare 529 * checked Exceptions). 530 * 531 * @param future the {@link Future} representing a value of interest. 532 * @return the value {@code future} represents. 533 */ 534 public static <T> T waitForFuture(Future<T> future) { 535 try { 536 return future.get(WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 537 } catch (ExecutionException e) { 538 // ExecutionException means this Future has an associated Exception that we should 539 // re-throw on the current thread. We throw the cause instead of ExecutionException, 540 // since ExecutionException itself isn't interesting, and might mislead those debugging 541 // test failures to suspect this method is the culprit (whereas the root cause is from 542 // another thread). 543 Throwable cause = e.getCause(); 544 // If the cause is an unchecked Throwable type, re-throw as-is. 545 if (cause instanceof Error) throw(Error) cause; 546 if (cause instanceof RuntimeException) throw(RuntimeException) cause; 547 // Otherwise, wrap this in an unchecked Exception so callers don't need to declare 548 // checked Exceptions. 549 throw new RuntimeException(cause); 550 } catch (InterruptedException | TimeoutException e) { 551 // Don't call e.getCause() for either of these. Unlike ExecutionException, these don't 552 // wrap the root cause, but rather are themselves interesting. Again, we wrap these 553 // checked Exceptions with an unchecked Exception for the caller's convenience. 554 // 555 // Although we might be tempted to handle InterruptedException by calling 556 // Thread.currentThread().interrupt(), this is not correct in this case. The interrupted 557 // thread was likely a different thread than the current thread, so there's nothing 558 // special we need to do. 559 throw new RuntimeException(e); 560 } 561 } 562 563 /** 564 * Takes an element out of the {@link BlockingQueue} (or times out). 565 */ 566 public static <T> T waitForNextQueueElement(BlockingQueue<T> queue) throws Exception { 567 T value = queue.poll(WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 568 if (value == null) { 569 // {@code null} is the special value which means {@link BlockingQueue#poll} has timed 570 // out (also: there's no risk for collision with real values, because BlockingQueue does 571 // not allow null entries). Instead of returning this special value, let's throw a 572 // proper TimeoutException. 573 throw new TimeoutException( 574 "Timeout while trying to take next entry from BlockingQueue"); 575 } 576 return value; 577 } 578 579 /** 580 * Clears the resource cache. Note that the cache is per-application, so this will clear the 581 * cache for all WebViews used. 582 */ 583 public void clearCacheOnUiThread(final AwContents awContents, final boolean includeDiskFiles) { 584 TestThreadUtils.runOnUiThreadBlocking(() -> awContents.clearCache(includeDiskFiles)); 585 } 586 587 /** 588 * Returns pure page scale. 589 */ 590 public float getScaleOnUiThread(final AwContents awContents) throws Exception { 591 return TestThreadUtils.runOnUiThreadBlocking(() -> awContents.getPageScaleFactor()); 592 } 593 594 /** 595 * Returns page scale multiplied by the screen density. 596 */ 597 public float getPixelScaleOnUiThread(final AwContents awContents) throws Exception { 598 return TestThreadUtils.runOnUiThreadBlocking(() -> awContents.getScale()); 599 } 600 601 /** 602 * Returns whether a user can zoom the page in. 603 */ 604 public boolean canZoomInOnUiThread(final AwContents awContents) throws Exception { 605 return TestThreadUtils.runOnUiThreadBlocking(() -> awContents.canZoomIn()); 606 } 607 608 /** 609 * Returns whether a user can zoom the page out. 610 */ 611 public boolean canZoomOutOnUiThread(final AwContents awContents) throws Exception { 612 return TestThreadUtils.runOnUiThreadBlocking(() -> awContents.canZoomOut()); 613 } 614 615 /** 616 * Loads the main html then triggers the popup window. 617 */ 618 public void triggerPopup(final AwContents parentAwContents, 619 TestAwContentsClient parentAwContentsClient, TestWebServer testWebServer, 620 String mainHtml, String popupHtml, String popupPath, String triggerScript) 621 throws Exception { 622 enableJavaScriptOnUiThread(parentAwContents); 623 TestThreadUtils.runOnUiThreadBlocking(() -> { 624 parentAwContents.getSettings().setSupportMultipleWindows(true); 625 parentAwContents.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); 626 }); 627 628 final String parentUrl = testWebServer.setResponse("/popupParent.html", mainHtml, null); 629 if (popupHtml != null) { 630 testWebServer.setResponse(popupPath, popupHtml, null); 631 } else { 632 testWebServer.setResponseWithNoContentStatus(popupPath); 633 } 634 635 parentAwContentsClient.getOnCreateWindowHelper().setReturnValue(true); 636 loadUrlSync(parentAwContents, parentAwContentsClient.getOnPageFinishedHelper(), parentUrl); 637 638 TestAwContentsClient.OnCreateWindowHelper onCreateWindowHelper = 639 parentAwContentsClient.getOnCreateWindowHelper(); 640 int currentCallCount = onCreateWindowHelper.getCallCount(); 641 TestThreadUtils.runOnUiThreadBlocking( 642 () -> parentAwContents.evaluateJavaScriptForTests(triggerScript, null)); 643 onCreateWindowHelper.waitForCallback( 644 currentCallCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 645 } 646 647 /** 648 * Supplies the popup window with AwContents then waits for the popup window to finish loading. 649 * @param parentAwContents Parent webview's AwContents. 650 */ 651 public PopupInfo connectPendingPopup(AwContents parentAwContents) throws Exception { 652 PopupInfo popupInfo = createPopupContents(parentAwContents); 653 loadPopupContents(parentAwContents, popupInfo, null); 654 return popupInfo; 655 } 656 657 /** 658 * Creates a popup window with AwContents. 659 */ 660 public PopupInfo createPopupContents(final AwContents parentAwContents) { 661 TestAwContentsClient popupContentsClient; 662 AwTestContainerView popupContainerView; 663 final AwContents popupContents; 664 popupContentsClient = new TestAwContentsClient(); 665 popupContainerView = createAwTestContainerViewOnMainSync(popupContentsClient); 666 popupContents = popupContainerView.getAwContents(); 667 enableJavaScriptOnUiThread(popupContents); 668 return new PopupInfo(popupContentsClient, popupContainerView, popupContents); 669 } 670 671 /** 672 * Waits for the popup window to finish loading. 673 * @param parentAwContents Parent webview's AwContents. 674 * @param info The PopupInfo. 675 * @param onCreateWindowHandler An instance of OnCreateWindowHandler. null if there isn't. 676 */ 677 public void loadPopupContents(final AwContents parentAwContents, PopupInfo info, 678 OnCreateWindowHandler onCreateWindowHandler) throws Exception { 679 TestAwContentsClient popupContentsClient = info.popupContentsClient; 680 AwTestContainerView popupContainerView = info.popupContainerView; 681 final AwContents popupContents = info.popupContents; 682 OnPageFinishedHelper onPageFinishedHelper = popupContentsClient.getOnPageFinishedHelper(); 683 int finishCallCount = onPageFinishedHelper.getCallCount(); 684 685 if (onCreateWindowHandler != null) onCreateWindowHandler.onCreateWindow(popupContents); 686 687 TestAwContentsClient.OnReceivedTitleHelper onReceivedTitleHelper = 688 popupContentsClient.getOnReceivedTitleHelper(); 689 int titleCallCount = onReceivedTitleHelper.getCallCount(); 690 691 TestThreadUtils.runOnUiThreadBlocking( 692 () -> parentAwContents.supplyContentsForPopup(popupContents)); 693 694 onPageFinishedHelper.waitForCallback( 695 finishCallCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 696 onReceivedTitleHelper.waitForCallback( 697 titleCallCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 698 } 699 700 private boolean testMethodHasAnnotation(Class<? extends Annotation> clazz) { 701 return mCurrentTestDescription.getAnnotation(clazz) != null ? true : false; 702 } 703 704 /** 705 * Factory class used in creation of test AwContents instances. Test cases 706 * can provide subclass instances to the createAwTest* methods in order to 707 * create an AwContents instance with injected test dependencies. 708 */ 709 public static class TestDependencyFactory extends AwContents.DependencyFactory { 710 public AwTestContainerView createAwTestContainerView( 711 AwTestRunnerActivity activity, boolean allowHardwareAcceleration) { 712 return new AwTestContainerView(activity, allowHardwareAcceleration); 713 } 714 715 public AwSettings createAwSettings(Context context, boolean supportsLegacyQuirks) { 716 return new AwSettings(context, false /* isAccessFromFileURLsGrantedByDefault */, 717 supportsLegacyQuirks, false /* allowEmptyDocumentPersistence */, 718 true /* allowGeolocationOnInsecureOrigins */, 719 false /* doNotUpdateSelectionOnMutatingSelectionRange */); 720 } 721 722 public AwContents createAwContents(AwBrowserContext browserContext, ViewGroup containerView, 723 Context context, InternalAccessDelegate internalAccessAdapter, 724 NativeDrawFunctorFactory nativeDrawFunctorFactory, AwContentsClient contentsClient, 725 AwSettings settings, DependencyFactory dependencyFactory) { 726 return new AwContents(browserContext, containerView, context, internalAccessAdapter, 727 nativeDrawFunctorFactory, contentsClient, settings, dependencyFactory); 728 } 729 } 730 731 /** 732 * POD object for holding references to helper objects of a popup window. 733 */ 734 public static class PopupInfo { 735 public final TestAwContentsClient popupContentsClient; 736 public final AwTestContainerView popupContainerView; 737 public final AwContents popupContents; 738 739 public PopupInfo(TestAwContentsClient popupContentsClient, 740 AwTestContainerView popupContainerView, AwContents popupContents) { 741 this.popupContentsClient = popupContentsClient; 742 this.popupContainerView = popupContainerView; 743 this.popupContents = popupContents; 744 } 745 } 746 } 747