1// Copyright 2012 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
5package org.chromium.android_webview.test;
6
7import android.app.Instrumentation;
8import android.content.Context;
9import android.test.ActivityInstrumentationTestCase2;
10import android.util.Log;
11
12import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout;
13
14import org.chromium.android_webview.AwBrowserContext;
15import org.chromium.android_webview.AwBrowserProcess;
16import org.chromium.android_webview.AwContents;
17import org.chromium.android_webview.AwContentsClient;
18import org.chromium.android_webview.AwSettings;
19import org.chromium.android_webview.test.util.JSUtils;
20import org.chromium.base.test.util.InMemorySharedPreferences;
21import org.chromium.content.browser.ContentSettings;
22import org.chromium.content.browser.test.util.CallbackHelper;
23import org.chromium.content.browser.test.util.Criteria;
24import org.chromium.content.browser.test.util.CriteriaHelper;
25import org.chromium.content_public.browser.LoadUrlParams;
26
27import java.util.Map;
28import java.util.concurrent.Callable;
29import java.util.concurrent.FutureTask;
30import java.util.concurrent.TimeUnit;
31import java.util.concurrent.atomic.AtomicReference;
32
33/**
34 * A base class for android_webview tests.
35 */
36public class AwTestBase
37        extends ActivityInstrumentationTestCase2<AwTestRunnerActivity> {
38    public static final long WAIT_TIMEOUT_MS = scaleTimeout(15000);
39    public static final int CHECK_INTERVAL = 100;
40    private static final String TAG = "AwTestBase";
41
42    public AwTestBase() {
43        super(AwTestRunnerActivity.class);
44    }
45
46    @Override
47    protected void setUp() throws Exception {
48        super.setUp();
49        if (needsBrowserProcessStarted()) {
50            final Context context = getActivity();
51            getInstrumentation().runOnMainSync(new Runnable() {
52                @Override
53                public void run() {
54                    AwBrowserProcess.start(context);
55                }
56            });
57        }
58    }
59
60    /* Override this to return false if the test doesn't want the browser startup sequence to
61     * be run automatically.
62     */
63    protected boolean needsBrowserProcessStarted() {
64        return true;
65    }
66
67    /**
68     * Runs a {@link Callable} on the main thread, blocking until it is
69     * complete, and returns the result. Calls
70     * {@link Instrumentation#waitForIdleSync()} first to help avoid certain
71     * race conditions.
72     *
73     * @param <R> Type of result to return
74     */
75    public <R> R runTestOnUiThreadAndGetResult(Callable<R> callable)
76            throws Exception {
77        FutureTask<R> task = new FutureTask<R>(callable);
78        getInstrumentation().waitForIdleSync();
79        getInstrumentation().runOnMainSync(task);
80        return task.get();
81    }
82
83    public void enableJavaScriptOnUiThread(final AwContents awContents) {
84        getInstrumentation().runOnMainSync(new Runnable() {
85            @Override
86            public void run() {
87                awContents.getSettings().setJavaScriptEnabled(true);
88            }
89        });
90    }
91
92    public void setNetworkAvailableOnUiThread(final AwContents awContents,
93            final boolean networkUp) {
94        getInstrumentation().runOnMainSync(new Runnable() {
95            @Override
96            public void run() {
97                awContents.setNetworkAvailable(networkUp);
98            }
99        });
100    }
101
102    /**
103     * Loads url on the UI thread and blocks until onPageFinished is called.
104     */
105    public void loadUrlSync(final AwContents awContents,
106                               CallbackHelper onPageFinishedHelper,
107                               final String url) throws Exception {
108        loadUrlSync(awContents, onPageFinishedHelper, url, null);
109    }
110
111    public void loadUrlSync(final AwContents awContents,
112                               CallbackHelper onPageFinishedHelper,
113                               final String url,
114                               final Map<String, String> extraHeaders) throws Exception {
115        int currentCallCount = onPageFinishedHelper.getCallCount();
116        loadUrlAsync(awContents, url, extraHeaders);
117        onPageFinishedHelper.waitForCallback(currentCallCount, 1, WAIT_TIMEOUT_MS,
118                TimeUnit.MILLISECONDS);
119    }
120
121    public void loadUrlSyncAndExpectError(final AwContents awContents,
122            CallbackHelper onPageFinishedHelper,
123            CallbackHelper onReceivedErrorHelper,
124            final String url) throws Exception {
125        int onErrorCallCount = onReceivedErrorHelper.getCallCount();
126        int onFinishedCallCount = onPageFinishedHelper.getCallCount();
127        loadUrlAsync(awContents, url);
128        onReceivedErrorHelper.waitForCallback(onErrorCallCount, 1, WAIT_TIMEOUT_MS,
129                TimeUnit.MILLISECONDS);
130        onPageFinishedHelper.waitForCallback(onFinishedCallCount, 1, WAIT_TIMEOUT_MS,
131                TimeUnit.MILLISECONDS);
132    }
133
134    /**
135     * Loads url on the UI thread but does not block.
136     */
137    public void loadUrlAsync(final AwContents awContents,
138                                final String url) throws Exception {
139        loadUrlAsync(awContents, url, null);
140    }
141
142    public void loadUrlAsync(final AwContents awContents,
143                                final String url,
144                                final Map<String, String> extraHeaders) {
145        getInstrumentation().runOnMainSync(new Runnable() {
146            @Override
147            public void run() {
148                LoadUrlParams params = new LoadUrlParams(url);
149                params.setExtraHeaders(extraHeaders);
150                awContents.loadUrl(params);
151            }
152        });
153    }
154
155    /**
156     * Posts url on the UI thread and blocks until onPageFinished is called.
157     */
158    public void postUrlSync(final AwContents awContents,
159            CallbackHelper onPageFinishedHelper, final String url,
160            byte[] postData) throws Exception {
161        int currentCallCount = onPageFinishedHelper.getCallCount();
162        postUrlAsync(awContents, url, postData);
163        onPageFinishedHelper.waitForCallback(currentCallCount, 1, WAIT_TIMEOUT_MS,
164                TimeUnit.MILLISECONDS);
165    }
166
167    /**
168     * Loads url on the UI thread but does not block.
169     */
170    public void postUrlAsync(final AwContents awContents,
171            final String url, byte[] postData) throws Exception {
172        class PostUrl implements Runnable {
173            byte[] mPostData;
174            public PostUrl(byte[] postData) {
175                mPostData = postData;
176            }
177            @Override
178            public void run() {
179                awContents.loadUrl(LoadUrlParams.createLoadHttpPostParams(url,
180                        mPostData));
181            }
182        }
183        getInstrumentation().runOnMainSync(new PostUrl(postData));
184    }
185
186    /**
187     * Loads data on the UI thread and blocks until onPageFinished is called.
188     */
189    public void loadDataSync(final AwContents awContents,
190                                CallbackHelper onPageFinishedHelper,
191                                final String data, final String mimeType,
192                                final boolean isBase64Encoded) throws Exception {
193        int currentCallCount = onPageFinishedHelper.getCallCount();
194        loadDataAsync(awContents, data, mimeType, isBase64Encoded);
195        onPageFinishedHelper.waitForCallback(currentCallCount, 1, WAIT_TIMEOUT_MS,
196                TimeUnit.MILLISECONDS);
197    }
198
199    public void loadDataSyncWithCharset(final AwContents awContents,
200                                           CallbackHelper onPageFinishedHelper,
201                                           final String data, final String mimeType,
202                                           final boolean isBase64Encoded, final String charset)
203            throws Exception {
204        int currentCallCount = onPageFinishedHelper.getCallCount();
205        getInstrumentation().runOnMainSync(new Runnable() {
206            @Override
207            public void run() {
208                awContents.loadUrl(LoadUrlParams.createLoadDataParams(
209                        data, mimeType, isBase64Encoded, charset));
210            }
211        });
212        onPageFinishedHelper.waitForCallback(currentCallCount, 1, WAIT_TIMEOUT_MS,
213                TimeUnit.MILLISECONDS);
214    }
215
216    /**
217     * Loads data on the UI thread but does not block.
218     */
219    public void loadDataAsync(final AwContents awContents, final String data,
220                                 final String mimeType, final boolean isBase64Encoded)
221            throws Exception {
222        getInstrumentation().runOnMainSync(new Runnable() {
223            @Override
224            public void run() {
225                awContents.loadUrl(LoadUrlParams.createLoadDataParams(
226                        data, mimeType, isBase64Encoded));
227            }
228        });
229    }
230
231    public void loadDataWithBaseUrlSync(final AwContents awContents,
232            CallbackHelper onPageFinishedHelper, final String data, final String mimeType,
233            final boolean isBase64Encoded, final String baseUrl,
234            final String historyUrl) throws Throwable {
235        int currentCallCount = onPageFinishedHelper.getCallCount();
236        loadDataWithBaseUrlAsync(awContents, data, mimeType, isBase64Encoded, baseUrl, historyUrl);
237        onPageFinishedHelper.waitForCallback(currentCallCount, 1, WAIT_TIMEOUT_MS,
238                TimeUnit.MILLISECONDS);
239    }
240
241    public void loadDataWithBaseUrlAsync(final AwContents awContents,
242            final String data, final String mimeType, final boolean isBase64Encoded,
243            final String baseUrl, final String historyUrl) throws Throwable {
244        runTestOnUiThread(new Runnable() {
245            @Override
246            public void run() {
247                awContents.loadUrl(LoadUrlParams.createLoadDataParamsWithBaseUrl(
248                        data, mimeType, isBase64Encoded, baseUrl, historyUrl));
249            }
250        });
251    }
252
253    /**
254     * Reloads the current page synchronously.
255     */
256    public void reloadSync(final AwContents awContents,
257                              CallbackHelper onPageFinishedHelper) throws Exception {
258        int currentCallCount = onPageFinishedHelper.getCallCount();
259        getInstrumentation().runOnMainSync(new Runnable() {
260            @Override
261            public void run() {
262                awContents.getNavigationController().reload(true);
263            }
264        });
265        onPageFinishedHelper.waitForCallback(currentCallCount, 1, WAIT_TIMEOUT_MS,
266                TimeUnit.MILLISECONDS);
267    }
268
269    /**
270     * Factory class used in creation of test AwContents instances.
271     *
272     * Test cases can provide subclass instances to the createAwTest* methods in order to create an
273     * AwContents instance with injected test dependencies.
274     */
275    public static class TestDependencyFactory extends AwContents.DependencyFactory {
276        public AwTestContainerView createAwTestContainerView(AwTestRunnerActivity activity) {
277            return new AwTestContainerView(activity, false);
278        }
279        public AwSettings createAwSettings(Context context, boolean supportsLegacyQuirks) {
280            return new AwSettings(context, false, supportsLegacyQuirks);
281        }
282    }
283
284    protected TestDependencyFactory createTestDependencyFactory() {
285        return new TestDependencyFactory();
286    }
287
288    public AwTestContainerView createAwTestContainerView(
289            final AwContentsClient awContentsClient) {
290        return createAwTestContainerView(awContentsClient, false);
291    }
292
293    public AwTestContainerView createAwTestContainerView(
294            final AwContentsClient awContentsClient, boolean supportsLegacyQuirks) {
295        AwTestContainerView testContainerView =
296                createDetachedAwTestContainerView(awContentsClient, supportsLegacyQuirks);
297        getActivity().addView(testContainerView);
298        testContainerView.requestFocus();
299        return testContainerView;
300    }
301
302    // The browser context needs to be a process-wide singleton.
303    private AwBrowserContext mBrowserContext =
304            new AwBrowserContext(new InMemorySharedPreferences());
305
306    public AwTestContainerView createDetachedAwTestContainerView(
307            final AwContentsClient awContentsClient) {
308        return createDetachedAwTestContainerView(awContentsClient, false);
309    }
310
311    public AwTestContainerView createDetachedAwTestContainerView(
312            final AwContentsClient awContentsClient, boolean supportsLegacyQuirks) {
313        final TestDependencyFactory testDependencyFactory = createTestDependencyFactory();
314        final AwTestContainerView testContainerView =
315            testDependencyFactory.createAwTestContainerView(getActivity());
316        AwSettings awSettings = testDependencyFactory.createAwSettings(getActivity(),
317                supportsLegacyQuirks);
318        testContainerView.initialize(new AwContents(
319                mBrowserContext, testContainerView, testContainerView.getContext(),
320                testContainerView.getInternalAccessDelegate(),
321                testContainerView.getNativeGLDelegate(), awContentsClient,
322                awSettings, testDependencyFactory));
323        return testContainerView;
324    }
325
326    public AwTestContainerView createAwTestContainerViewOnMainSync(
327            final AwContentsClient client) throws Exception {
328        return createAwTestContainerViewOnMainSync(client, false);
329    }
330
331    public AwTestContainerView createAwTestContainerViewOnMainSync(
332            final AwContentsClient client, final boolean supportsLegacyQuirks) throws Exception {
333        final AtomicReference<AwTestContainerView> testContainerView =
334                new AtomicReference<AwTestContainerView>();
335        getInstrumentation().runOnMainSync(new Runnable() {
336            @Override
337            public void run() {
338                testContainerView.set(createAwTestContainerView(client, supportsLegacyQuirks));
339            }
340        });
341        return testContainerView.get();
342    }
343
344    public void destroyAwContentsOnMainSync(final AwContents awContents) {
345        if (awContents == null) return;
346        getInstrumentation().runOnMainSync(new Runnable() {
347            @Override
348            public void run() {
349                awContents.destroy();
350            }
351        });
352    }
353
354    public String getTitleOnUiThread(final AwContents awContents) throws Exception {
355        return runTestOnUiThreadAndGetResult(new Callable<String>() {
356            @Override
357            public String call() throws Exception {
358                return awContents.getTitle();
359            }
360        });
361    }
362
363    public ContentSettings getContentSettingsOnUiThread(
364            final AwContents awContents) throws Exception {
365        return runTestOnUiThreadAndGetResult(new Callable<ContentSettings>() {
366            @Override
367            public ContentSettings call() throws Exception {
368                return awContents.getContentViewCore().getContentSettings();
369            }
370        });
371    }
372
373    public AwSettings getAwSettingsOnUiThread(
374            final AwContents awContents) throws Exception {
375        return runTestOnUiThreadAndGetResult(new Callable<AwSettings>() {
376            @Override
377            public AwSettings call() throws Exception {
378                return awContents.getSettings();
379            }
380        });
381    }
382
383    /**
384     * Executes the given snippet of JavaScript code within the given ContentView. Returns the
385     * result of its execution in JSON format.
386     */
387    public String executeJavaScriptAndWaitForResult(final AwContents awContents,
388            TestAwContentsClient viewClient, final String code) throws Exception {
389        return JSUtils.executeJavaScriptAndWaitForResult(this, awContents,
390                viewClient.getOnEvaluateJavaScriptResultHelper(),
391                code);
392    }
393
394    /**
395     * Wrapper around CriteriaHelper.pollForCriteria. This uses AwTestBase-specifc timeouts and
396     * treats timeouts and exceptions as test failures automatically.
397     */
398    public static void poll(final Callable<Boolean> callable) throws Exception {
399        assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
400            @Override
401            public boolean isSatisfied() {
402                try {
403                    return callable.call();
404                } catch (Throwable e) {
405                    Log.e(TAG, "Exception while polling.", e);
406                    return false;
407                }
408            }
409        }, WAIT_TIMEOUT_MS, CHECK_INTERVAL));
410    }
411
412    /**
413     * Wrapper around {@link AwTestBase#poll()} but runs the callable on the UI thread.
414     */
415    public void pollOnUiThread(final Callable<Boolean> callable) throws Exception {
416        poll(new Callable<Boolean>() {
417            @Override
418            public Boolean call() throws Exception {
419                return runTestOnUiThreadAndGetResult(callable);
420            }
421        });
422    }
423
424    /**
425     * Clears the resource cache. Note that the cache is per-application, so this will clear the
426     * cache for all WebViews used.
427     */
428    public void clearCacheOnUiThread(
429            final AwContents awContents,
430            final boolean includeDiskFiles) throws Exception {
431        getInstrumentation().runOnMainSync(new Runnable() {
432            @Override
433            public void run() {
434                awContents.clearCache(includeDiskFiles);
435            }
436        });
437    }
438
439    /**
440     * Returns pure page scale.
441     */
442    public float getScaleOnUiThread(final AwContents awContents) throws Exception {
443        return runTestOnUiThreadAndGetResult(new Callable<Float>() {
444            @Override
445            public Float call() throws Exception {
446                return awContents.getPageScaleFactor();
447            }
448        });
449    }
450
451    /**
452     * Returns page scale multiplied by the screen density.
453     */
454    public float getPixelScaleOnUiThread(final AwContents awContents) throws Exception {
455        return runTestOnUiThreadAndGetResult(new Callable<Float>() {
456            @Override
457            public Float call() throws Exception {
458                return awContents.getScale();
459            }
460        });
461    }
462
463    /**
464     * Returns whether a user can zoom the page in.
465     */
466    public boolean canZoomInOnUiThread(final AwContents awContents) throws Exception {
467        return runTestOnUiThreadAndGetResult(new Callable<Boolean>() {
468            @Override
469            public Boolean call() throws Exception {
470                return awContents.canZoomIn();
471            }
472        });
473    }
474
475    /**
476     * Returns whether a user can zoom the page out.
477     */
478    public boolean canZoomOutOnUiThread(final AwContents awContents) throws Exception {
479        return runTestOnUiThreadAndGetResult(new Callable<Boolean>() {
480            @Override
481            public Boolean call() throws Exception {
482                return awContents.canZoomOut();
483            }
484        });
485    }
486
487}
488