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