| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.android_webview.test; |
| |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.hamcrest.Matchers.greaterThan; |
| |
| import static org.chromium.android_webview.test.AwActivityTestRule.WAIT_TIMEOUT_MS; |
| import static org.chromium.android_webview.test.OnlyRunIn.ProcessMode.MULTI_PROCESS; |
| import static org.chromium.android_webview.test.OnlyRunIn.ProcessMode.SINGLE_PROCESS; |
| |
| import android.annotation.SuppressLint; |
| import android.content.ComponentCallbacks2; |
| import android.content.Context; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.util.Pair; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.webkit.JavascriptInterface; |
| |
| import androidx.annotation.NonNull; |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.filters.LargeTest; |
| import androidx.test.filters.MediumTest; |
| import androidx.test.filters.SmallTest; |
| |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.util.concurrent.SettableFuture; |
| |
| import org.junit.Assert; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.TestRule; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| import org.junit.runners.Parameterized.UseParametersRunnerFactory; |
| |
| import org.chromium.android_webview.AwContents; |
| import org.chromium.android_webview.AwRenderProcess; |
| import org.chromium.android_webview.AwSettings; |
| import org.chromium.android_webview.common.AwFeatures; |
| import org.chromium.android_webview.common.PlatformServiceBridge; |
| import org.chromium.android_webview.renderer_priority.RendererPriority; |
| import org.chromium.android_webview.test.TestAwContentsClient.OnDownloadStartHelper; |
| import org.chromium.android_webview.test.util.CommonResources; |
| import org.chromium.android_webview.test.util.GraphicsTestUtils; |
| import org.chromium.base.BaseFeatures; |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.FakeTimeTestRule; |
| import org.chromium.base.Log; |
| import org.chromium.base.TimeUtils; |
| import org.chromium.base.test.util.CallbackHelper; |
| import org.chromium.base.test.util.CommandLineFlags; |
| import org.chromium.base.test.util.DisabledTest; |
| import org.chromium.base.test.util.DoNotBatch; |
| import org.chromium.base.test.util.Feature; |
| import org.chromium.base.test.util.Features; |
| import org.chromium.base.test.util.HistogramWatcher; |
| import org.chromium.base.test.util.MinAndroidSdkLevel; |
| import org.chromium.content_public.browser.test.util.RenderProcessHostUtils; |
| import org.chromium.content_public.browser.test.util.TestThreadUtils; |
| import org.chromium.content_public.browser.test.util.TouchCommon; |
| import org.chromium.content_public.common.ContentSwitches; |
| import org.chromium.content_public.common.ContentUrlConstants; |
| import org.chromium.net.test.EmbeddedTestServer; |
| import org.chromium.net.test.util.TestWebServer; |
| |
| import java.io.InputStream; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.Semaphore; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.function.BiFunction; |
| import java.util.function.Predicate; |
| |
| /** AwContents tests. */ |
| @RunWith(Parameterized.class) |
| @UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class) |
| @DoNotBatch(reason = "Tests that need browser start are incompatible with @Batch") |
| public class AwContentsTest extends AwParameterizedTest { |
| private static final String TAG = "AwContentsTest"; |
| |
| @Rule public AwActivityTestRule mActivityTestRule; |
| |
| public AwContentsTest(AwSettingsMutation param) { |
| mActivityTestRule = |
| new AwActivityTestRule(param.getMutation()) { |
| // Allow specific tests to use vulkan. |
| @Override |
| public boolean needsBrowserProcessStarted() { |
| return false; |
| } |
| }; |
| } |
| |
| @Rule public FakeTimeTestRule mFakeTimeTestRule = new FakeTimeTestRule(); |
| @Rule public TestRule mProcessor = new Features.InstrumentationProcessor(); |
| |
| private TestAwContentsClient mContentsClient = new TestAwContentsClient(); |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testCreateDestroy() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| // NOTE this test runs on UI thread, so we cannot call any async methods. |
| mActivityTestRule.runOnUiThread( |
| () -> |
| mActivityTestRule |
| .createAwTestContainerView(mContentsClient) |
| .getAwContents() |
| .destroy()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testCreateLoadPageDestroy() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView awTestContainerView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| mActivityTestRule.loadDataSync( |
| awTestContainerView.getAwContents(), |
| mContentsClient.getOnPageFinishedHelper(), |
| CommonResources.ABOUT_HTML, |
| "text/html", |
| false); |
| |
| mActivityTestRule.destroyAwContentsOnMainSync(awTestContainerView.getAwContents()); |
| // It should be safe to call destroy multiple times. |
| mActivityTestRule.destroyAwContentsOnMainSync(awTestContainerView.getAwContents()); |
| } |
| |
| @Test |
| @LargeTest |
| @Feature({"AndroidWebView"}) |
| public void testCreateLoadDestroyManyTimes() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| for (int i = 0; i < 10; ++i) { |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| AwContents awContents = testView.getAwContents(); |
| |
| mActivityTestRule.loadUrlSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL); |
| mActivityTestRule.destroyAwContentsOnMainSync(awContents); |
| } |
| } |
| |
| @Test |
| @LargeTest |
| @Feature({"AndroidWebView"}) |
| public void testCreateLoadDestroyManyAtOnce() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView[] views = new AwTestContainerView[10]; |
| |
| for (int i = 0; i < views.length; ++i) { |
| views[i] = mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| mActivityTestRule.loadUrlSync( |
| views[i].getAwContents(), |
| mContentsClient.getOnPageFinishedHelper(), |
| ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL); |
| } |
| |
| for (int i = 0; i < views.length; ++i) { |
| mActivityTestRule.destroyAwContentsOnMainSync(views[i].getAwContents()); |
| views[i] = null; |
| } |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testWebViewApisFailGracefullyAfterDestruction() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| mActivityTestRule.runOnUiThread( |
| () -> { |
| AwContents awContents = |
| mActivityTestRule |
| .createAwTestContainerView(mContentsClient) |
| .getAwContents(); |
| awContents.destroy(); |
| |
| // The documentation for WebView#destroy() reads "This method should be called |
| // after this WebView has been removed from the view system. No other methods |
| // may be called on this WebView after destroy". |
| // However, some apps do not respect that restriction so we need to ensure that |
| // we fail gracefully and do not crash when APIs are invoked after destruction. |
| // Due to the large number of APIs we only test a representative selection here. |
| awContents.clearHistory(); |
| Assert.assertNull(awContents.getOriginalUrl()); |
| Assert.assertNull(awContents.getNavigationHistory()); |
| awContents.loadUrl("http://www.google.com"); |
| awContents.findAllAsync("search"); |
| Assert.assertNull(awContents.getUrl()); |
| Assert.assertFalse(awContents.canGoBack()); |
| awContents.disableJavascriptInterfacesInspection(); |
| awContents.invokeZoomPicker(); |
| awContents.onResume(); |
| awContents.stopLoading(); |
| awContents.onWindowVisibilityChanged(View.VISIBLE); |
| awContents.requestFocus(); |
| awContents.isMultiTouchZoomSupported(); |
| awContents.setOverScrollMode(View.OVER_SCROLL_NEVER); |
| awContents.pauseTimers(); |
| awContents.onContainerViewScrollChanged(200, 200, 100, 100); |
| awContents.computeScroll(); |
| awContents.onMeasure(100, 100); |
| awContents.onDraw(new Canvas()); |
| awContents.getMostRecentProgress(); |
| Assert.assertEquals(0, awContents.computeHorizontalScrollOffset()); |
| Assert.assertEquals(0, awContents.getContentWidthCss()); |
| awContents.onKeyUp( |
| KeyEvent.KEYCODE_BACK, |
| new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU)); |
| }); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testUseAwSettingsAfterDestroy() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView awTestContainerView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| AwSettings awSettings = |
| mActivityTestRule.getAwSettingsOnUiThread(awTestContainerView.getAwContents()); |
| mActivityTestRule.loadDataSync( |
| awTestContainerView.getAwContents(), |
| mContentsClient.getOnPageFinishedHelper(), |
| CommonResources.ABOUT_HTML, |
| "text/html", |
| false); |
| mActivityTestRule.destroyAwContentsOnMainSync(awTestContainerView.getAwContents()); |
| |
| // AwSettings should still be usable even after native side is destroyed. |
| String newFontFamily = "serif"; |
| awSettings.setStandardFontFamily(newFontFamily); |
| Assert.assertEquals(newFontFamily, awSettings.getStandardFontFamily()); |
| boolean newBlockNetworkLoads = !awSettings.getBlockNetworkLoads(); |
| awSettings.setBlockNetworkLoads(newBlockNetworkLoads); |
| Assert.assertEquals(newBlockNetworkLoads, awSettings.getBlockNetworkLoads()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testGoBackGoForwardWithoutSessionHistory() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| mActivityTestRule.runOnUiThread( |
| () -> { |
| AwContents awContents = |
| mActivityTestRule |
| .createAwTestContainerView(mContentsClient) |
| .getAwContents(); |
| |
| Assert.assertFalse(awContents.canGoBack()); |
| Assert.assertFalse(awContents.canGoForward()); |
| // If no back/forward entries exist, then calling these should do nothing and |
| // not crash or fail asserts. |
| awContents.goBack(); |
| awContents.goForward(); |
| }); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testBackgroundColorInDarkMode() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| mActivityTestRule.runOnUiThread( |
| () -> { |
| AwContents awContents = |
| mActivityTestRule |
| .createAwTestContainerView(mContentsClient) |
| .getAwContents(); |
| AwSettings awSettings = awContents.getSettings(); |
| |
| Assert.assertEquals( |
| awContents.getEffectiveBackgroundColorForTesting(), Color.WHITE); |
| |
| awSettings.setForceDarkMode(AwSettings.FORCE_DARK_ON); |
| Assert.assertTrue(awSettings.isForceDarkApplied()); |
| Assert.assertEquals( |
| awContents.getEffectiveBackgroundColorForTesting(), Color.BLACK); |
| |
| awContents.setBackgroundColor(Color.RED); |
| Assert.assertEquals( |
| awContents.getEffectiveBackgroundColorForTesting(), Color.RED); |
| |
| awContents.destroy(); |
| Assert.assertEquals( |
| awContents.getEffectiveBackgroundColorForTesting(), Color.RED); |
| }); |
| } |
| |
| private int callDocumentHasImagesSync(final AwContents awContents) |
| throws Throwable, InterruptedException { |
| // Set up a container to hold the result object and a semaphore to |
| // make the test wait for the result. |
| final AtomicInteger val = new AtomicInteger(); |
| final Semaphore s = new Semaphore(0); |
| final Message msg = |
| Message.obtain( |
| new Handler(Looper.getMainLooper()) { |
| @Override |
| public void handleMessage(Message msg) { |
| val.set(msg.arg1); |
| s.release(); |
| } |
| }); |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync(() -> awContents.documentHasImages(msg)); |
| Assert.assertTrue( |
| s.tryAcquire(AwActivityTestRule.SCALED_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); |
| int result = val.get(); |
| return result; |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testDocumentHasImages() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| AwContents awContents = testView.getAwContents(); |
| |
| final CallbackHelper loadHelper = mContentsClient.getOnPageFinishedHelper(); |
| |
| final String mime = "text/html"; |
| final String emptyDoc = "<head/><body/>"; |
| final String imageDoc = "<head/><body><img/><img/></body>"; |
| |
| // Make sure a document that does not have images returns 0 |
| mActivityTestRule.loadDataSync(awContents, loadHelper, emptyDoc, mime, false); |
| int result = callDocumentHasImagesSync(awContents); |
| Assert.assertEquals(0, result); |
| |
| // Make sure a document that does have images returns 1 |
| mActivityTestRule.loadDataSync(awContents, loadHelper, imageDoc, mime, false); |
| result = callDocumentHasImagesSync(awContents); |
| Assert.assertEquals(1, result); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| @SkipMutations(reason = "This test depends on AwSettings.setCacheMode()") |
| public void testClearCacheMemoryAndDisk() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| final AwTestContainerView testContainer = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testContainer.getAwContents(); |
| |
| TestWebServer webServer = TestWebServer.start(); |
| try { |
| final String pagePath = "/clear_cache_test.html"; |
| List<Pair<String, String>> headers = new ArrayList<Pair<String, String>>(); |
| // Set Cache-Control headers to cache this request. One century should be long enough. |
| headers.add(Pair.create("Cache-Control", "max-age=3153600000")); |
| headers.add(Pair.create("Last-Modified", "Wed, 3 Oct 2012 00:00:00 GMT")); |
| final String pageUrl = |
| webServer.setResponse(pagePath, "<html><body>foo</body></html>", headers); |
| |
| // First load to populate cache. |
| mActivityTestRule.clearCacheOnUiThread(awContents, true); |
| mActivityTestRule.loadUrlSync( |
| awContents, mContentsClient.getOnPageFinishedHelper(), pageUrl); |
| Assert.assertEquals(1, webServer.getRequestCount(pagePath)); |
| |
| // Load about:blank so next load is not treated as reload by webkit and force |
| // revalidate with the server. |
| mActivityTestRule.loadUrlSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL); |
| |
| // No clearCache call, so should be loaded from cache. |
| mActivityTestRule.loadUrlSync( |
| awContents, mContentsClient.getOnPageFinishedHelper(), pageUrl); |
| Assert.assertEquals(1, webServer.getRequestCount(pagePath)); |
| |
| // Same as above. |
| mActivityTestRule.loadUrlSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL); |
| |
| // Clear cache, so should hit server again. |
| mActivityTestRule.clearCacheOnUiThread(awContents, true); |
| mActivityTestRule.loadUrlSync( |
| awContents, mContentsClient.getOnPageFinishedHelper(), pageUrl); |
| Assert.assertEquals(2, webServer.getRequestCount(pagePath)); |
| } finally { |
| webServer.shutdown(); |
| } |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testClearCacheInQuickSuccession() { |
| mActivityTestRule.startBrowserProcess(); |
| final AwTestContainerView testContainer = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(new TestAwContentsClient()); |
| final AwContents awContents = testContainer.getAwContents(); |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> { |
| for (int i = 0; i < 10; ++i) { |
| awContents.clearCache(true); |
| } |
| }); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testGetFavicon() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwContents.setShouldDownloadFavicons(); |
| final AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| TestWebServer webServer = TestWebServer.start(); |
| try { |
| final String faviconUrl = |
| webServer.setResponseBase64( |
| "/" + CommonResources.FAVICON_FILENAME, |
| CommonResources.FAVICON_DATA_BASE64, |
| CommonResources.getImagePngHeaders(false)); |
| final String pageUrl = |
| webServer.setResponse( |
| "/favicon.html", CommonResources.FAVICON_STATIC_HTML, null); |
| |
| // The getFavicon will return the right icon a certain time after |
| // the page load completes which makes it slightly hard to test. |
| final Bitmap defaultFavicon = awContents.getFavicon(); |
| |
| mActivityTestRule.getAwSettingsOnUiThread(awContents).setImagesEnabled(true); |
| mActivityTestRule.loadUrlSync( |
| awContents, mContentsClient.getOnPageFinishedHelper(), pageUrl); |
| |
| mActivityTestRule.pollUiThread( |
| () -> |
| awContents.getFavicon() != null |
| && !awContents.getFavicon().sameAs(defaultFavicon)); |
| |
| final Object originalFaviconSource = (new URL(faviconUrl)).getContent(); |
| final Bitmap originalFavicon = |
| BitmapFactory.decodeStream((InputStream) originalFaviconSource); |
| Assert.assertNotNull(originalFavicon); |
| |
| Assert.assertTrue(awContents.getFavicon().sameAs(originalFavicon)); |
| |
| } finally { |
| webServer.shutdown(); |
| } |
| } |
| |
| @Test |
| @Feature({"AndroidWebView", "Downloads"}) |
| @SmallTest |
| @SkipMutations(reason = "This test depends on AwSettings.setUserAgentString()") |
| public void testDownload() throws Throwable { |
| downloadAndCheck(null); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView", "Downloads"}) |
| @SmallTest |
| public void testDownloadWithCustomUserAgent() throws Throwable { |
| downloadAndCheck("Custom User Agent"); |
| } |
| |
| private void downloadAndCheck(String customUserAgent) throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| AwContents awContents = testView.getAwContents(); |
| |
| if (customUserAgent != null) { |
| AwSettings awSettings = mActivityTestRule.getAwSettingsOnUiThread(awContents); |
| awSettings.setUserAgentString(customUserAgent); |
| } |
| |
| final String data = "download data"; |
| final String contentDisposition = "attachment;filename=\"download.txt\""; |
| final String mimeType = "text/plain"; |
| |
| List<Pair<String, String>> downloadHeaders = new ArrayList<Pair<String, String>>(); |
| downloadHeaders.add(Pair.create("Content-Disposition", contentDisposition)); |
| downloadHeaders.add(Pair.create("Content-Type", mimeType)); |
| downloadHeaders.add(Pair.create("Content-Length", Integer.toString(data.length()))); |
| |
| TestWebServer webServer = TestWebServer.start(); |
| try { |
| final String pageUrl = webServer.setResponse("/download.txt", data, downloadHeaders); |
| final OnDownloadStartHelper downloadStartHelper = |
| mContentsClient.getOnDownloadStartHelper(); |
| final int callCount = downloadStartHelper.getCallCount(); |
| mActivityTestRule.loadUrlAsync(awContents, pageUrl); |
| downloadStartHelper.waitForCallback(callCount); |
| |
| Assert.assertEquals(pageUrl, downloadStartHelper.getUrl()); |
| Assert.assertEquals(contentDisposition, downloadStartHelper.getContentDisposition()); |
| Assert.assertEquals(mimeType, downloadStartHelper.getMimeType()); |
| Assert.assertEquals(data.length(), downloadStartHelper.getContentLength()); |
| Assert.assertFalse(downloadStartHelper.getUserAgent().isEmpty()); |
| if (customUserAgent != null) { |
| Assert.assertEquals(customUserAgent, downloadStartHelper.getUserAgent()); |
| } else { |
| Assert.assertEquals( |
| downloadStartHelper.getUserAgent(), AwSettings.getDefaultUserAgent()); |
| } |
| } finally { |
| webServer.shutdown(); |
| } |
| } |
| |
| @Test |
| @Feature({"AndroidWebView", "setNetworkAvailable"}) |
| @SmallTest |
| public void testSetNetworkAvailable() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| AwContents awContents = testView.getAwContents(); |
| String script = "navigator.onLine"; |
| |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| mActivityTestRule.loadUrlSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL); |
| |
| // Default to "online". |
| Assert.assertEquals( |
| "true", |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| awContents, mContentsClient, script)); |
| |
| // Forcing "offline". |
| AwActivityTestRule.setNetworkAvailableOnUiThread(awContents, false); |
| Assert.assertEquals( |
| "false", |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| awContents, mContentsClient, script)); |
| |
| // Forcing "online". |
| AwActivityTestRule.setNetworkAvailableOnUiThread(awContents, true); |
| Assert.assertEquals( |
| "true", |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| awContents, mContentsClient, script)); |
| } |
| |
| static class JavaScriptObject { |
| |
| private CallbackHelper mCallbackHelper; |
| |
| public JavaScriptObject(CallbackHelper callbackHelper) { |
| mCallbackHelper = callbackHelper; |
| } |
| |
| @JavascriptInterface |
| public void run() { |
| mCallbackHelper.notifyCalled(); |
| } |
| } |
| |
| @Test |
| @Feature({"AndroidWebView", "Android-JavaBridge"}) |
| @SmallTest |
| public void testJavaBridge() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| final AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final CallbackHelper callback = new CallbackHelper(); |
| |
| AwContents awContents = testView.getAwContents(); |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| AwActivityTestRule.addJavascriptInterfaceOnUiThread( |
| awContents, new JavaScriptObject(callback), "bridge"); |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| awContents, mContentsClient, "window.bridge.run();"); |
| callback.waitForCallback(0, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testEscapingOfErrorPage() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| AwContents awContents = testView.getAwContents(); |
| String script = "window.failed == true"; |
| |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| CallbackHelper onPageFinishedHelper = mContentsClient.getOnPageFinishedHelper(); |
| int currentCallCount = onPageFinishedHelper.getCallCount(); |
| mActivityTestRule.loadUrlAsync( |
| awContents, |
| "file:///file-that-does-not-exist#<script>window.failed = true;</script>"); |
| onPageFinishedHelper.waitForCallback( |
| currentCallCount, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| |
| Assert.assertEquals( |
| "false", |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| awContents, mContentsClient, script)); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testCanInjectHeaders() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| final AwTestContainerView testContainer = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testContainer.getAwContents(); |
| |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| |
| EmbeddedTestServer testServer = |
| EmbeddedTestServer.createAndStartServer( |
| InstrumentationRegistry.getInstrumentation().getContext()); |
| |
| String url = testServer.getURL("/echoheader?X-foo"); |
| final Map<String, String> extraHeaders = new HashMap<String, String>(); |
| extraHeaders.put("X-foo", "bar"); |
| mActivityTestRule.loadUrlSync( |
| awContents, mContentsClient.getOnPageFinishedHelper(), url, extraHeaders); |
| String xfoo = |
| mActivityTestRule.getJavaScriptResultBodyTextContent(awContents, mContentsClient); |
| Assert.assertEquals("bar", xfoo); |
| url = testServer.getURL("/echoheader?Referer"); |
| mActivityTestRule.loadUrlSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| url, |
| ImmutableMap.of("Referer", "http://www.example.com/")); |
| String referer = |
| mActivityTestRule.getJavaScriptResultBodyTextContent(awContents, mContentsClient); |
| Assert.assertEquals("http://www.example.com/", referer); |
| } |
| |
| // This is a meta test that we don't accidentally turn off hardware |
| // acceleration in instrumentation tests without notice. Do not add the |
| // @DisableHardwareAcceleration annotation for this test. |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testHardwareModeWorks() { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testContainer = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| Assert.assertTrue(testContainer.isHardwareAccelerated()); |
| Assert.assertTrue(testContainer.isBackedByHardwareView()); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testBasicCookieFunctionality() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| AwContents awContents = testView.getAwContents(); |
| |
| TestWebServer webServer = TestWebServer.start(); |
| try { |
| List<Pair<String, String>> responseHeaders = CommonResources.getTextHtmlHeaders(true); |
| final String cookie = "key=value"; |
| responseHeaders.add(Pair.create("Set-Cookie", cookie)); |
| final String url = |
| webServer.setResponse( |
| "/" + CommonResources.ABOUT_FILENAME, |
| CommonResources.ABOUT_HTML, |
| responseHeaders); |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| mActivityTestRule.loadUrlSync( |
| awContents, mContentsClient.getOnPageFinishedHelper(), url); |
| |
| final String script = "document.cookie"; |
| Assert.assertEquals( |
| "\"key=value\"", |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| awContents, mContentsClient, script)); |
| } finally { |
| webServer.shutdown(); |
| } |
| } |
| |
| /** Verifies that Web Notifications and the Push API are not exposed in WebView. */ |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testPushAndNotificationsDisabled() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| AwContents awContents = testView.getAwContents(); |
| |
| String script = "window.Notification || window.PushManager"; |
| |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| mActivityTestRule.loadUrlSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL); |
| Assert.assertEquals( |
| "null", |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| awContents, mContentsClient, script)); |
| } |
| |
| private @RendererPriority int getRendererPriorityOnUiThread(final AwContents awContents) |
| throws Exception { |
| return TestThreadUtils.runOnUiThreadBlocking( |
| () -> awContents.getEffectivePriorityForTesting()); |
| } |
| |
| private void setRendererPriorityOnUiThread( |
| final AwContents awContents, |
| final @RendererPriority int priority, |
| final boolean waivedWhenNotVisible) |
| throws Throwable { |
| mActivityTestRule.runOnUiThread( |
| () -> awContents.setRendererPriorityPolicy(priority, waivedWhenNotVisible)); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| @OnlyRunIn(MULTI_PROCESS) |
| @CommandLineFlags.Add(ContentSwitches.RENDER_PROCESS_LIMIT + "=1") |
| public void testForegroundPriorityOneProcess() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| final AwTestContainerView view1 = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents contents1 = view1.getAwContents(); |
| final AwTestContainerView view2 = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents contents2 = view2.getAwContents(); |
| |
| mActivityTestRule.loadUrlSync( |
| contents1, |
| mContentsClient.getOnPageFinishedHelper(), |
| ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL); |
| mActivityTestRule.loadUrlSync( |
| contents2, |
| mContentsClient.getOnPageFinishedHelper(), |
| ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL); |
| |
| // Process should start out high. |
| Assert.assertEquals(RendererPriority.HIGH, getRendererPriorityOnUiThread(contents1)); |
| Assert.assertEquals(RendererPriority.HIGH, getRendererPriorityOnUiThread(contents2)); |
| |
| // Set one to low. Process should take max priority of contents, so still high. |
| setRendererPriorityOnUiThread(contents1, RendererPriority.LOW, false); |
| Assert.assertEquals(RendererPriority.HIGH, getRendererPriorityOnUiThread(contents1)); |
| Assert.assertEquals(RendererPriority.HIGH, getRendererPriorityOnUiThread(contents2)); |
| |
| // Set both to low and check. |
| setRendererPriorityOnUiThread(contents2, RendererPriority.LOW, false); |
| Assert.assertEquals(RendererPriority.LOW, getRendererPriorityOnUiThread(contents1)); |
| Assert.assertEquals(RendererPriority.LOW, getRendererPriorityOnUiThread(contents2)); |
| |
| // Set both to waive and check. |
| setRendererPriorityOnUiThread(contents1, RendererPriority.WAIVED, false); |
| setRendererPriorityOnUiThread(contents2, RendererPriority.WAIVED, false); |
| Assert.assertEquals(RendererPriority.WAIVED, getRendererPriorityOnUiThread(contents1)); |
| Assert.assertEquals(RendererPriority.WAIVED, getRendererPriorityOnUiThread(contents2)); |
| |
| // Set one to high and check. |
| setRendererPriorityOnUiThread(contents1, RendererPriority.HIGH, false); |
| Assert.assertEquals(RendererPriority.HIGH, getRendererPriorityOnUiThread(contents1)); |
| Assert.assertEquals(RendererPriority.HIGH, getRendererPriorityOnUiThread(contents2)); |
| |
| // Destroy contents with high priority, and process should fall back to low. |
| // Destroy posts on UI, but getRendererPriorityOnUiThread posts after, so there should |
| // be no flakiness and no need for polling. |
| mActivityTestRule.destroyAwContentsOnMainSync(contents1); |
| Assert.assertEquals(RendererPriority.WAIVED, getRendererPriorityOnUiThread(contents2)); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| @OnlyRunIn(MULTI_PROCESS) |
| @CommandLineFlags.Add(ContentSwitches.RENDER_PROCESS_LIMIT + "=2") |
| public void testForegroundPriorityTwoProcesses() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| final AwTestContainerView view1 = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents contents1 = view1.getAwContents(); |
| final AwTestContainerView view2 = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents contents2 = view2.getAwContents(); |
| |
| mActivityTestRule.loadUrlSync( |
| contents1, |
| mContentsClient.getOnPageFinishedHelper(), |
| ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL); |
| mActivityTestRule.loadUrlSync( |
| contents2, |
| mContentsClient.getOnPageFinishedHelper(), |
| ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL); |
| |
| // Process should start out high. |
| Assert.assertEquals(RendererPriority.HIGH, getRendererPriorityOnUiThread(contents1)); |
| Assert.assertEquals(RendererPriority.HIGH, getRendererPriorityOnUiThread(contents2)); |
| |
| // Set one to low. Other should not be affected. |
| setRendererPriorityOnUiThread(contents1, RendererPriority.LOW, false); |
| Assert.assertEquals(RendererPriority.LOW, getRendererPriorityOnUiThread(contents1)); |
| Assert.assertEquals(RendererPriority.HIGH, getRendererPriorityOnUiThread(contents2)); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| @OnlyRunIn(MULTI_PROCESS) |
| public void testBackgroundPriority() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| final AwContents awContents = |
| mActivityTestRule |
| .createAwTestContainerViewOnMainSync(mContentsClient) |
| .getAwContents(); |
| Assert.assertEquals(RendererPriority.HIGH, getRendererPriorityOnUiThread(awContents)); |
| |
| mActivityTestRule.runOnUiThread(() -> awContents.onPause()); |
| Assert.assertEquals(RendererPriority.HIGH, getRendererPriorityOnUiThread(awContents)); |
| |
| setRendererPriorityOnUiThread( |
| awContents, RendererPriority.HIGH, /* waivedWhenNotVisible= */ true); |
| Assert.assertEquals(RendererPriority.WAIVED, getRendererPriorityOnUiThread(awContents)); |
| |
| mActivityTestRule.runOnUiThread(() -> awContents.onResume()); |
| Assert.assertEquals(RendererPriority.HIGH, getRendererPriorityOnUiThread(awContents)); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| @OnlyRunIn(MULTI_PROCESS) |
| public void testPauseDestroyResume() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| mActivityTestRule.runOnUiThread( |
| () -> { |
| AwContents awContents; |
| awContents = |
| mActivityTestRule |
| .createAwTestContainerView(mContentsClient) |
| .getAwContents(); |
| awContents.pauseTimers(); |
| awContents.pauseTimers(); |
| awContents.destroy(); |
| awContents = |
| mActivityTestRule |
| .createAwTestContainerView(mContentsClient) |
| .getAwContents(); |
| awContents.resumeTimers(); |
| }); |
| } |
| |
| private AwRenderProcess getRenderProcessOnUiThread(final AwContents awContents) |
| throws Exception { |
| return TestThreadUtils.runOnUiThreadBlocking(() -> awContents.getRenderProcess()); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| @OnlyRunIn(MULTI_PROCESS) |
| public void testRenderProcessInMultiProcessMode() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| final AwRenderProcess preLoadRenderProcess = getRenderProcessOnUiThread(awContents); |
| Assert.assertNotNull(preLoadRenderProcess); |
| |
| mActivityTestRule.loadUrlSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL); |
| |
| final AwRenderProcess postLoadRenderProcess = getRenderProcessOnUiThread(awContents); |
| Assert.assertEquals(preLoadRenderProcess, postLoadRenderProcess); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| @OnlyRunIn(SINGLE_PROCESS) |
| public void testNoRenderProcessInSingleProcessMode() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| mActivityTestRule.loadUrlSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL); |
| |
| final AwRenderProcess renderProcess = getRenderProcessOnUiThread(awContents); |
| Assert.assertEquals(renderProcess, null); |
| } |
| |
| /** |
| * Regression test for https://crbug.com/732976. Load a data URL, then immediately after that |
| * load a javascript URL. The data URL navigation shouldn't be blocked. |
| */ |
| @Test |
| @LargeTest |
| @Feature({"AndroidWebView"}) |
| public void testJavaScriptUrlAfterLoadData() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| mActivityTestRule.runOnUiThread( |
| () -> { |
| // Run javascript navigation immediately, without waiting for the completion of |
| // data URL. |
| awContents.loadData("<html>test</html>", "text/html", "utf-8"); |
| awContents.loadUrl("javascript: void(0)"); |
| }); |
| |
| mContentsClient |
| .getOnPageFinishedHelper() |
| .waitForCallback(0, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| Assert.assertEquals("data:text/html,<html>test</html>", awContents.getLastCommittedUrl()); |
| |
| TestAwContentsClient.AddMessageToConsoleHelper consoleHelper = |
| mContentsClient.getAddMessageToConsoleHelper(); |
| Assert.assertEquals(0, consoleHelper.getMessages().size()); |
| } |
| |
| /** |
| * Regression test for https://crbug.com/1226748. Call stopLoading() before any page has been |
| * loaded, load a page, and then load a JavaScript URL. The JavaScript URL should execute. |
| */ |
| @Test |
| @LargeTest |
| @Feature({"AndroidWebView"}) |
| public void testJavaScriptUrlAfterStopLoading() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| // It should always be safe to call stopLoading() even if we haven't loaded anything yet. |
| mActivityTestRule.stopLoading(awContents); |
| mActivityTestRule.loadUrlSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL); |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| mActivityTestRule.loadUrlAsync(awContents, "javascript:location.reload()"); |
| |
| // Wait for the page to reload and trigger another onPageFinished() |
| mContentsClient |
| .getOnPageFinishedHelper() |
| .waitForCallback(0, 2, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| } |
| |
| /** |
| * Regression test for https://crbug.com/1231883. Call stopLoading() before any page has been |
| * loaded, load a page, and then call evaluateJavaScript. The JavaScript code should execute. |
| */ |
| @Test |
| @LargeTest |
| @Feature({"AndroidWebView"}) |
| public void testEvaluateJavaScriptAfterStopLoading() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| // It should always be safe to call stopLoading() even if we haven't loaded anything yet. |
| mActivityTestRule.stopLoading(awContents); |
| mActivityTestRule.loadUrlSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL); |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| mActivityTestRule.runOnUiThread( |
| () -> { |
| // We specifically call AwContents.evaluateJavaScript() rather than the |
| // AwActivityTestRule helper methods to make sure we're using the same code path |
| // as production. |
| awContents.evaluateJavaScript("location.reload()", null); |
| }); |
| |
| // Wait for the page to reload and trigger another onPageFinished() |
| mContentsClient |
| .getOnPageFinishedHelper() |
| .waitForCallback(0, 2, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| |
| // Verify the callback actually contains the execution result. |
| final SettableFuture<String> jsResult = SettableFuture.create(); |
| mActivityTestRule.runOnUiThread( |
| () -> { |
| // We specifically call AwContents.evaluateJavaScript() rather than the |
| // AwActivityTestRule helper methods to make sure we're using the same code path |
| // as production. |
| awContents.evaluateJavaScript("1 + 2", jsResult::set); |
| }); |
| Assert.assertEquals( |
| "JavaScript expression result should be correct", |
| "3", |
| AwActivityTestRule.waitForFuture(jsResult)); |
| } |
| |
| /** |
| * Regression test for https://crbug.com/1145717. Load a URL that requires fixing and verify |
| * that the legacy behavior is preserved (i.e. that the URL is fixed + that no crashes happen in |
| * the product). |
| * |
| * <p>The main test verification is that there are no crashes. In particular, this test tries to |
| * verify that the `loadUrl` call above won't trigger: |
| * <li>NOTREACHED and DwoC in content::NavigationRequest's constructor for about: scheme |
| * navigations that aren't about:blank nor about:srcdoc |
| * <li>CHECK in content::NavigationRequest::GetOriginForURLLoaderFactory caused by the mismatch |
| * between the result of this method and the "about:" process lock. |
| */ |
| @Test |
| @LargeTest |
| @Feature({"AndroidWebView"}) |
| public void testLoadUrlAboutVersion() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| mActivityTestRule.runOnUiThread( |
| () -> { |
| // "about:safe-browsing" will be rewritten by |
| // components.url_formatter.UrlFormatter.fixupUrl into |
| // "chrome://safe-browsing/". |
| // |
| // Note that chrome://safe-browsing/ is one of very few chrome://... URLs that |
| // work |
| // in Android WebView. In particular, chrome://version/ wouldn't work. |
| awContents.loadUrl("about:safe-browsing"); |
| }); |
| |
| mContentsClient |
| .getOnPageFinishedHelper() |
| .waitForCallback(0, 1, WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| Assert.assertEquals("chrome://safe-browsing/", awContents.getLastCommittedUrl()); |
| } |
| |
| private void doHardwareRenderingSmokeTest() throws Throwable { |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| doHardwareRenderingSmokeTest(testView); |
| } |
| |
| private void doHardwareRenderingSmokeTest(AwTestContainerView testView) throws Throwable { |
| doHardwareRenderingSmokeTest(testView, 128, 128, 128); |
| } |
| |
| private void doHardwareRenderingSmokeTest(AwTestContainerView testView, int r, int g, int b) |
| throws Throwable { |
| String html = |
| String.format( |
| "<html>" |
| + " <body style=\"" |
| + " padding: 0;" |
| + " margin: 0;" |
| + " display: grid;" |
| + " display: grid;" |
| + " grid-template-columns: 50%% 50%%;" |
| + " grid-template-rows: 50%% 50%%;\">" |
| + " <div style=\"background-color: rgb(255, 0, 0);\"></div>" |
| + " <div style=\"background-color: rgb(0, 255, 0);\"></div>" |
| + " <div style=\"background-color: rgb(0, 0, 255);\"></div>" |
| + " <div style=\"background-color: rgb(%d, %d, %d);\"></div>" |
| + " </body>" |
| + "</html>", |
| r, g, b); |
| mActivityTestRule.loadDataSync( |
| testView.getAwContents(), |
| mContentsClient.getOnPageFinishedHelper(), |
| html, |
| "text/html", |
| false); |
| mActivityTestRule.waitForVisualStateCallback(testView.getAwContents()); |
| |
| int[] expectedQuadrantColors = { |
| Color.rgb(255, 0, 0), Color.rgb(0, 255, 0), Color.rgb(0, 0, 255), Color.rgb(r, g, b) |
| }; |
| |
| GraphicsTestUtils.pollForQuadrantColors(testView, expectedQuadrantColors); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @MediumTest |
| public void testHardwareRenderingSmokeTest() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| doHardwareRenderingSmokeTest(); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @MediumTest |
| @MinAndroidSdkLevel(Build.VERSION_CODES.P) |
| public void testHardwareRenderingSmokeTestVulkanWhereSupported() throws Throwable { |
| // Manually curated list. |
| final String[] supportedModels = { |
| "Pixel", "Pixel 2", "Pixel 3", "Pixel 4a", |
| }; |
| if (!Arrays.asList(supportedModels).contains(Build.MODEL)) { |
| Log.w(TAG, "Skipping vulkan test on unknown device: " + Build.MODEL); |
| return; |
| } |
| mActivityTestRule.startBrowserProcessWithVulkan(); |
| doHardwareRenderingSmokeTest(); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testFixupOctothorpesInLoadDataContent() { |
| mActivityTestRule.startBrowserProcess(); |
| // If there are no octothorpes the function should have no effect. |
| final String noOctothorpeString = "<div id='foo1'>This content has no octothorpe</div>"; |
| Assert.assertEquals( |
| noOctothorpeString, |
| AwContents.fixupOctothorpesInLoadDataContent(noOctothorpeString)); |
| |
| // One '#' followed by a valid DOM id requires us to duplicate it into a real fragment. |
| Assert.assertEquals("abc%23A#A", AwContents.fixupOctothorpesInLoadDataContent("abc#A")); |
| Assert.assertEquals("abc%23a#a", AwContents.fixupOctothorpesInLoadDataContent("abc#a")); |
| Assert.assertEquals("abc%23Aa#Aa", AwContents.fixupOctothorpesInLoadDataContent("abc#Aa")); |
| Assert.assertEquals("abc%23aA#aA", AwContents.fixupOctothorpesInLoadDataContent("abc#aA")); |
| Assert.assertEquals( |
| "abc%23a1-_:.#a1-_:.", AwContents.fixupOctothorpesInLoadDataContent("abc#a1-_:.")); |
| |
| // One '#' followed by an invalid DOM id just means we encode the '#'. |
| Assert.assertEquals("abc%231", AwContents.fixupOctothorpesInLoadDataContent("abc#1")); |
| Assert.assertEquals("abc%231a", AwContents.fixupOctothorpesInLoadDataContent("abc#1a")); |
| Assert.assertEquals( |
| "abc%23not valid", AwContents.fixupOctothorpesInLoadDataContent("abc#not valid")); |
| Assert.assertEquals("abc%23a@", AwContents.fixupOctothorpesInLoadDataContent("abc#a@")); |
| |
| // Multiple '#', whether or not they have a valid DOM id afterwards, just means we encode |
| // the '#'. |
| Assert.assertEquals("abc%23%23a", AwContents.fixupOctothorpesInLoadDataContent("abc##a")); |
| Assert.assertEquals("abc%23a%23b", AwContents.fixupOctothorpesInLoadDataContent("abc#a#b")); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testLoadDataOctothorpeHandling() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| // Before Android Q, the loadData API is expected to handle the encoding for users. |
| boolean encodeOctothorpes = |
| ContextUtils.getApplicationContext().getApplicationInfo().targetSdkVersion |
| < Build.VERSION_CODES.Q; |
| |
| // A URL with no '#' character. |
| mActivityTestRule.loadDataSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| "<html>test</html>", |
| "text/html", |
| false); |
| Assert.assertEquals("data:text/html,<html>test</html>", awContents.getLastCommittedUrl()); |
| |
| // A URL with one '#' character. |
| mActivityTestRule.loadDataSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| "<html>test#foo</html>", |
| "text/html", |
| false); |
| String expectedUrl = |
| encodeOctothorpes |
| ? "data:text/html,<html>test%23foo</html>" |
| : "data:text/html,<html>test#foo</html>"; |
| Assert.assertEquals(expectedUrl, awContents.getLastCommittedUrl()); |
| |
| // A URL with many '#' characters. |
| mActivityTestRule.loadDataSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| "<html>test#foo#bar#</html>", |
| "text/html", |
| false); |
| expectedUrl = |
| encodeOctothorpes |
| ? "data:text/html,<html>test%23foo%23bar%23</html>" |
| : "data:text/html,<html>test#foo#bar#</html>"; |
| Assert.assertEquals(expectedUrl, awContents.getLastCommittedUrl()); |
| |
| // An already encoded '#' character. |
| mActivityTestRule.loadDataSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| "<html>test%23foo</html>", |
| "text/html", |
| false); |
| Assert.assertEquals( |
| "data:text/html,<html>test%23foo</html>", awContents.getLastCommittedUrl()); |
| |
| // A URL with a valid fragment. Before Q, this must be manipulated so that it renders the |
| // same and still scrolls to the fragment location. |
| if (encodeOctothorpes) { |
| String contents = "<div style='height: 5000px'></div><a id='target'>Target</a>#target"; |
| mActivityTestRule.loadDataSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| contents, |
| "text/html", |
| false); |
| Assert.assertEquals( |
| "data:text/html,<div style='height: 5000px'></div><a id='target'>Target</a>" |
| + "%23target#target", |
| awContents.getLastCommittedUrl()); |
| // TODO(smcgruer): I can physically see that this has scrolled on the test page, and |
| // have traced scrolling through PaintLayerScrollableArea, but I don't know how to check |
| // it. |
| } |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @MediumTest |
| public void testLoadsJsModule() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| AwSettings awSettings = mActivityTestRule.getAwSettingsOnUiThread(awContents); |
| |
| // This test is specifically about relative file urls |
| awSettings.setAllowFileAccess(true); |
| awSettings.setAllowFileAccessFromFileURLs(true); |
| |
| // This test runs some javascript to verify if it passes |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| |
| // Using a future to wait to see if the js module was loaded or not. |
| // The page in the test will expect this object. |
| final SettableFuture<Boolean> fetchResultFuture = SettableFuture.create(); |
| Object injectedObject = |
| new Object() { |
| @JavascriptInterface |
| public void success() { |
| fetchResultFuture.set(true); |
| } |
| |
| @JavascriptInterface |
| public void error() { |
| fetchResultFuture.set(false); |
| } |
| }; |
| AwActivityTestRule.addJavascriptInterfaceOnUiThread( |
| awContents, injectedObject, "injectedObject"); |
| |
| final String url = "file:///android_asset/page_with_module.html"; |
| mActivityTestRule.loadUrlAsync(awContents, url); |
| |
| Assert.assertTrue(AwActivityTestRule.waitForFuture(fetchResultFuture)); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testLoadUrlRecordsScheme_http() { |
| // No need to spin up a web server, since we don't care if the load ever succeeds. |
| final String httpUrlWithNoRealPage = "http://some.origin.test/some/path.html"; |
| loadUrlAndCheckScheme(httpUrlWithNoRealPage, AwContents.UrlScheme.HTTP_SCHEME); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testLoadUrlRecordsScheme_javascript() { |
| loadUrlAndCheckScheme( |
| "javascript:console.log('message')", AwContents.UrlScheme.JAVASCRIPT_SCHEME); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testLoadUrlRecordsScheme_fileAndroidAsset() { |
| loadUrlAndCheckScheme( |
| "file:///android_asset/some/asset/page.html", |
| AwContents.UrlScheme.FILE_ANDROID_ASSET_SCHEME); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testLoadUrlRecordsScheme_fileRegular() { |
| loadUrlAndCheckScheme("file:///some/path/on/disk.html", AwContents.UrlScheme.FILE_SCHEME); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testLoadUrlRecordsScheme_data() { |
| loadUrlAndCheckScheme( |
| "data:text/html,<html><body>foo</body></html>", AwContents.UrlScheme.DATA_SCHEME); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testLoadUrlRecordsScheme_blank() { |
| loadUrlAndCheckScheme("about:blank", AwContents.UrlScheme.EMPTY); |
| } |
| |
| private void loadUrlAndCheckScheme(String url, @AwContents.UrlScheme int expectedSchemeEnum) { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| HistogramWatcher histogramExpectation = |
| HistogramWatcher.newSingleRecordWatcher( |
| AwContents.LOAD_URL_SCHEME_HISTOGRAM_NAME, expectedSchemeEnum); |
| |
| // Note: we use async because not all loads emit onPageFinished. This relies on the UMA |
| // metric being logged in the synchronous part of loadUrl(). |
| mActivityTestRule.loadUrlAsync(awContents, url); |
| histogramExpectation.assertExpected(); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testFindAllAsyncEmptySearchString() { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| try { |
| awContents.findAllAsync(null); |
| Assert.fail("A null searchString should cause an exception to be thrown"); |
| } catch (IllegalArgumentException e) { |
| // expected |
| } |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| public void testInsertNullVisualStateCallback() { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| try { |
| awContents.insertVisualStateCallback(0, null); |
| Assert.fail("A null VisualStateCallback should cause an exception to be thrown"); |
| } catch (IllegalArgumentException e) { |
| // expected |
| } |
| } |
| |
| // This test verifies that Private Network Access' secure context |
| // restriction (feature flag BlockInsecurePrivateNetworkRequests) does not |
| // apply to Webview: insecure private network requests are allowed. |
| // |
| // This is a regression test for crbug.com/1255675. |
| @Test |
| @Feature({"AndroidWebView"}) |
| @CommandLineFlags.Add(ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1") |
| @SmallTest |
| public void testInsecurePrivateNetworkAccess() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| final AwTestContainerView testContainer = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testContainer.getAwContents(); |
| |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| |
| // This SettableFuture and its accompanying injected object allows us to |
| // synchronize on the fetch result. |
| final SettableFuture<Boolean> fetchResultFuture = SettableFuture.create(); |
| Object injectedObject = |
| new Object() { |
| @JavascriptInterface |
| public void success() { |
| fetchResultFuture.set(true); |
| } |
| |
| @JavascriptInterface |
| public void error() { |
| fetchResultFuture.set(false); |
| } |
| }; |
| AwActivityTestRule.addJavascriptInterfaceOnUiThread( |
| awContents, injectedObject, "injectedObject"); |
| |
| EmbeddedTestServer testServer = |
| EmbeddedTestServer.createAndStartServer( |
| InstrumentationRegistry.getInstrumentation().getContext()); |
| |
| // Need to avoid http://localhost, which is considered secure, so we |
| // use http://foo.test, which resolves to 127.0.0.1 thanks to the |
| // host resolver rules command-line flag. |
| // |
| // The resulting document is a non-secure context in the public IP |
| // address space. If the secure context restriction were applied, it |
| // would not be allowed to fetch subresources from localhost. |
| String url = |
| testServer.getURLWithHostName( |
| "foo.test", "/set-header?Content-Security-Policy: treat-as-public-address"); |
| |
| mActivityTestRule.loadUrlSync(awContents, mContentsClient.getOnPageFinishedHelper(), url); |
| |
| // Fetch a subresource from the same server, whose IP address is still |
| // 127.0.0.1, thus belonging to the local IP address space. |
| // This should succeed. |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| awContents, |
| mContentsClient, |
| "fetch('/defaultresponse')" |
| + ".then(() => { injectedObject.success() })" |
| + ".catch((err) => { " |
| + " console.log(err); " |
| + " injectedObject.error(); " |
| + "})"); |
| |
| Assert.assertTrue(AwActivityTestRule.waitForFuture(fetchResultFuture)); |
| } |
| |
| private static final String HELLO_WORLD_URL = "/android_webview/test/data/hello_world.html"; |
| private static final String HELLO_WORLD_TITLE = "Hello, World!"; |
| private static final String WEBUI_URL = "chrome://safe-browsing"; |
| private static final String WEBUI_TITLE = "Safe Browsing"; |
| |
| // Check that we can navigate between a regular web page and a WebUI page |
| // that's available on AW (chrome://safe-browsing), and that the WebUI page |
| // loads in its own locked renderer process when in multi-process mode. |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| @OnlyRunIn(MULTI_PROCESS) |
| public void testWebUIUsesDedicatedProcessInMultiProcessMode() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| |
| EmbeddedTestServer testServer = |
| EmbeddedTestServer.createAndStartServer( |
| InstrumentationRegistry.getInstrumentation().getContext()); |
| final String pageUrl = testServer.getURL(HELLO_WORLD_URL); |
| mActivityTestRule.loadUrlSync( |
| awContents, mContentsClient.getOnPageFinishedHelper(), pageUrl); |
| Assert.assertEquals(HELLO_WORLD_TITLE, mActivityTestRule.getTitleOnUiThread(awContents)); |
| final AwRenderProcess rendererProcess1 = getRenderProcessOnUiThread(awContents); |
| Assert.assertNotNull(rendererProcess1); |
| // Until AW gets site isolation, ordinary web content should not be |
| // locked to origin. |
| boolean isLocked = |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> rendererProcess1.isProcessLockedToSiteForTesting()); |
| Assert.assertFalse("Initial renderer process should not be locked", isLocked); |
| mActivityTestRule.loadUrlSync( |
| awContents, mContentsClient.getOnPageFinishedHelper(), WEBUI_URL); |
| Assert.assertEquals(WEBUI_TITLE, mActivityTestRule.getTitleOnUiThread(awContents)); |
| final AwRenderProcess webuiProcess = getRenderProcessOnUiThread(awContents); |
| Assert.assertNotEquals(rendererProcess1, webuiProcess); |
| // WebUI pages should be locked to origin even on AW. |
| isLocked = |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> webuiProcess.isProcessLockedToSiteForTesting()); |
| Assert.assertTrue("WebUI process should be locked", isLocked); |
| mActivityTestRule.loadUrlSync( |
| awContents, mContentsClient.getOnPageFinishedHelper(), pageUrl); |
| final AwRenderProcess rendererProcess2 = getRenderProcessOnUiThread(awContents); |
| Assert.assertEquals(HELLO_WORLD_TITLE, mActivityTestRule.getTitleOnUiThread(awContents)); |
| Assert.assertNotEquals(rendererProcess2, webuiProcess); |
| isLocked = |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> rendererProcess2.isProcessLockedToSiteForTesting()); |
| Assert.assertFalse("Final renderer process should not be locked", isLocked); |
| } |
| |
| // In single-process mode, navigations to WebUI should work, but WebUI does |
| // not gets process-isolated. |
| @Test |
| @Feature({"AndroidWebView"}) |
| @SmallTest |
| @OnlyRunIn(SINGLE_PROCESS) |
| public void testWebUILoadsWithoutProcessIsolationInSingleProcessMode() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| |
| EmbeddedTestServer testServer = |
| EmbeddedTestServer.createAndStartServer( |
| InstrumentationRegistry.getInstrumentation().getContext()); |
| final String pageUrl = testServer.getURL(HELLO_WORLD_URL); |
| mActivityTestRule.loadUrlSync( |
| awContents, mContentsClient.getOnPageFinishedHelper(), pageUrl); |
| Assert.assertEquals(HELLO_WORLD_TITLE, mActivityTestRule.getTitleOnUiThread(awContents)); |
| final AwRenderProcess rendererProcess1 = getRenderProcessOnUiThread(awContents); |
| Assert.assertNull(rendererProcess1); |
| mActivityTestRule.loadUrlSync( |
| awContents, mContentsClient.getOnPageFinishedHelper(), WEBUI_URL); |
| Assert.assertEquals(WEBUI_TITLE, mActivityTestRule.getTitleOnUiThread(awContents)); |
| final AwRenderProcess webuiProcess = getRenderProcessOnUiThread(awContents); |
| Assert.assertNull(webuiProcess); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @MediumTest |
| @OnlyRunIn(MULTI_PROCESS) |
| @CommandLineFlags.Add(ContentSwitches.SITE_PER_PROCESS) |
| @DisabledTest(message = "https://crbug.com/1246585") |
| public void testOutOfProcessIframeSmokeTest() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| TestWebServer webServer = TestWebServer.start(); |
| try { |
| // Destination iframe has blue color. |
| final String iframeDestinationPath = |
| webServer.setResponse( |
| "/iframe_destination.html", |
| "<html><body style=\"background-color:rgb(0,0,255);\"></body></html>", |
| null); |
| // Initial iframe has red color with a full-page link to navigate to destination. |
| final String iframePath = |
| webServer.setResponse( |
| "/iframe.html", |
| "<html><body style=\"background-color:rgb(255,0,0);\">" |
| + "<a href=\"" |
| + iframeDestinationPath |
| + "\" " |
| + "style=\"width:100%;height:100%;display:block;\"></a>" |
| + "</body></html>", |
| null); |
| // Main frame has green color at the top half, and iframe in the bottom half. |
| final String pageHtml = |
| "<html><body><div" |
| + " style=\"width:100%;height:50%;background-color:rgb(0,255,0);\"></div><iframe" |
| + " style=\"width:100%;height:50%;\" src=\"" |
| + iframePath |
| + "\"></iframe>" |
| + "</body></html>"; |
| |
| // Iframes are loaded with origin of the test server, and the main page is loaded with |
| // origin http://foo.bar. This ensures that the main and iframe are different renderer |
| // processes when site isolation is enabled. |
| mActivityTestRule.loadDataWithBaseUrlSync( |
| awContents, |
| mContentsClient.getOnPageFinishedHelper(), |
| pageHtml, |
| "text/html", |
| false, |
| "http://foo.bar", |
| null); |
| |
| // Check initial iframe is displayed. |
| int[] expectedQuadrantColors = { |
| Color.rgb(0, 255, 0), |
| Color.rgb(0, 255, 0), |
| Color.rgb(255, 0, 0), |
| Color.rgb(255, 0, 0), |
| }; |
| GraphicsTestUtils.pollForQuadrantColors(testView, expectedQuadrantColors); |
| assertThat(RenderProcessHostUtils.getCurrentRenderProcessCount(), greaterThan(1)); |
| |
| // Click iframe to navigate. This exercises hit testing code paths. |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> { |
| int width = testView.getWidth(); |
| int height = testView.getHeight(); |
| TouchCommon.singleClickView(testView, width / 2, height * 3 / 4); |
| }); |
| |
| // Check destination iframe is displayed. |
| expectedQuadrantColors = |
| new int[] { |
| Color.rgb(0, 255, 0), |
| Color.rgb(0, 255, 0), |
| Color.rgb(0, 0, 255), |
| Color.rgb(0, 0, 255), |
| }; |
| GraphicsTestUtils.pollForQuadrantColors(testView, expectedQuadrantColors); |
| assertThat(RenderProcessHostUtils.getCurrentRenderProcessCount(), greaterThan(1)); |
| } finally { |
| webServer.shutdown(); |
| } |
| } |
| |
| private class FakePostDelayedTask implements BiFunction<Runnable, Long, Void> { |
| |
| @Override |
| public Void apply(Runnable runnable, Long delay) { |
| long time = TimeUtils.uptimeMillis() + delay; |
| mTasks.add(new Pair<Runnable, Long>(runnable, time)); |
| return null; |
| } |
| |
| public void fastForwardBy(long delay) { |
| mFakeTimeTestRule.advanceMillis(delay); |
| final long now = TimeUtils.uptimeMillis(); |
| Predicate<Pair<Runnable, Long>> deadlinePassed = |
| (Pair<Runnable, Long> p) -> { |
| return p.second <= now; |
| }; |
| |
| // Tasks running can post other tasks, do it in stages to prevent concurrent |
| // modification errors. |
| var toRun = new ArrayList<Pair<Runnable, Long>>(); |
| for (var p : mTasks) { |
| if (deadlinePassed.test(p)) { |
| toRun.add(p); |
| } |
| } |
| mTasks.removeIf(deadlinePassed); |
| for (var p : toRun) { |
| p.first.run(); |
| } |
| } |
| |
| public int getPendingTasksCount() { |
| return mTasks.size(); |
| } |
| |
| private List<Pair<Runnable, Long>> mTasks = new ArrayList<Pair<Runnable, Long>>(); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @MediumTest |
| @Features.EnableFeatures({AwFeatures.WEBVIEW_CLEAR_FUNCTOR_IN_BACKGROUND}) |
| public void testClearDrawFunctorInBackground() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| AwContents.resetRecordMemoryForTesting(); |
| |
| // Load a page to ensure that at least one draw has happened. |
| doHardwareRenderingSmokeTest(testView); |
| Assert.assertTrue(awContents.hasDrawFunctor()); |
| |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> { |
| var postTask = new FakePostDelayedTask(); |
| awContents.setPostDelayedTaskForTesting(postTask); |
| awContents.onWindowVisibilityChanged(View.INVISIBLE); |
| |
| // Delayed release task. |
| Assert.assertEquals(1, postTask.getPendingTasksCount()); |
| |
| postTask.fastForwardBy(AwContents.FUNCTOR_RECLAIM_DELAY_MS); |
| // Metrics task is still pending. |
| Assert.assertEquals(1, postTask.getPendingTasksCount()); |
| Assert.assertFalse(awContents.hasDrawFunctor()); |
| |
| awContents.onWindowVisibilityChanged(View.VISIBLE); |
| Assert.assertFalse(awContents.hasDrawFunctor()); |
| |
| // Metrics task will not report histograms because we went back to foreground in |
| // the meantime. |
| var histograms = |
| HistogramWatcher.newBuilder() |
| .expectNoRecords(AwContents.PSS_HISTOGRAM) |
| .expectNoRecords(AwContents.PRIVATE_DIRTY_HISTOGRAM) |
| .build(); |
| postTask.fastForwardBy(AwContents.METRICS_COLLECTION_DELAY_MS); |
| Assert.assertEquals(0, postTask.getPendingTasksCount()); |
| histograms.assertExpected(); |
| }); |
| |
| // Rendering still works. |
| doHardwareRenderingSmokeTest(testView, 42, 42, 42); |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> { |
| Assert.assertTrue(awContents.hasDrawFunctor()); |
| }); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @MediumTest |
| @Features.EnableFeatures({AwFeatures.WEBVIEW_CLEAR_FUNCTOR_IN_BACKGROUND}) |
| public void testClearDrawFunctorInBackgroundMultipleTransitions() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwContents.resetRecordMemoryForTesting(); |
| |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| // Load a page to ensure that at least one draw has happened. |
| doHardwareRenderingSmokeTest(testView); |
| Assert.assertTrue(awContents.hasDrawFunctor()); |
| |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> { |
| var postTask = new FakePostDelayedTask(); |
| awContents.setPostDelayedTaskForTesting(postTask); |
| awContents.onWindowVisibilityChanged(View.INVISIBLE); |
| |
| Assert.assertEquals(1, postTask.getPendingTasksCount()); |
| |
| postTask.fastForwardBy(AwContents.FUNCTOR_RECLAIM_DELAY_MS / 2); |
| awContents.onWindowVisibilityChanged(View.VISIBLE); |
| awContents.onWindowVisibilityChanged(View.INVISIBLE); |
| Assert.assertEquals(1, postTask.getPendingTasksCount()); |
| postTask.fastForwardBy(AwContents.FUNCTOR_RECLAIM_DELAY_MS / 2); |
| |
| // Not enough continuous time in background. |
| Assert.assertTrue(awContents.hasDrawFunctor()); |
| // But there is still a task pending. |
| Assert.assertEquals(1, postTask.getPendingTasksCount()); |
| |
| // Multiple transitions do not post multiple tasks. |
| awContents.onWindowVisibilityChanged(View.VISIBLE); |
| awContents.onWindowVisibilityChanged(View.INVISIBLE); |
| Assert.assertEquals(1, postTask.getPendingTasksCount()); |
| |
| // Functor is reclaimed after enough continuous time in background. |
| postTask.fastForwardBy(AwContents.FUNCTOR_RECLAIM_DELAY_MS); |
| Assert.assertFalse(awContents.hasDrawFunctor()); |
| |
| // Metrics task. |
| var histograms = |
| HistogramWatcher.newBuilder() |
| .expectAnyRecord(AwContents.PSS_HISTOGRAM) |
| .expectAnyRecord(AwContents.PRIVATE_DIRTY_HISTOGRAM) |
| .build(); |
| Assert.assertEquals(1, postTask.getPendingTasksCount()); |
| postTask.fastForwardBy(AwContents.METRICS_COLLECTION_DELAY_MS); |
| Assert.assertEquals(0, postTask.getPendingTasksCount()); |
| histograms.assertExpected(); |
| }); |
| |
| // Not testing rendering here, because all the back and forth advanced the virtual clock too |
| // much, the test would time out. |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @MediumTest |
| @Features.DisableFeatures({AwFeatures.WEBVIEW_CLEAR_FUNCTOR_IN_BACKGROUND}) |
| public void testDoeNotClearDrawFunctorInBackground() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwContents.resetRecordMemoryForTesting(); |
| |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| // Load a page to ensure that at least one draw has happened. |
| doHardwareRenderingSmokeTest(testView); |
| Assert.assertTrue(awContents.hasDrawFunctor()); |
| |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> { |
| var postTask = new FakePostDelayedTask(); |
| awContents.setPostDelayedTaskForTesting(postTask); |
| awContents.onWindowVisibilityChanged(View.INVISIBLE); |
| |
| // Background cleanup task is posted even when the feature is disabled. |
| Assert.assertEquals(1, postTask.getPendingTasksCount()); |
| postTask.fastForwardBy(AwContents.FUNCTOR_RECLAIM_DELAY_MS); |
| // But the functor is not cleared. |
| Assert.assertTrue(awContents.hasDrawFunctor()); |
| |
| // Metrics task. |
| var histograms = |
| HistogramWatcher.newBuilder() |
| .expectAnyRecord(AwContents.PSS_HISTOGRAM) |
| .expectAnyRecord(AwContents.PRIVATE_DIRTY_HISTOGRAM) |
| .build(); |
| Assert.assertEquals(1, postTask.getPendingTasksCount()); |
| postTask.fastForwardBy(AwContents.METRICS_COLLECTION_DELAY_MS); |
| Assert.assertEquals(0, postTask.getPendingTasksCount()); |
| histograms.assertExpected(); |
| |
| awContents.onWindowVisibilityChanged(View.VISIBLE); |
| Assert.assertEquals(0, postTask.getPendingTasksCount()); |
| Assert.assertTrue(awContents.hasDrawFunctor()); |
| }); |
| |
| // Rendering still works. |
| doHardwareRenderingSmokeTest(testView, 42, 42, 42); |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> { |
| Assert.assertTrue(awContents.hasDrawFunctor()); |
| }); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @MediumTest |
| @Features.EnableFeatures({AwFeatures.WEBVIEW_CLEAR_FUNCTOR_IN_BACKGROUND}) |
| public void testClearFunctorOnBackgroundMemorySignal() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwContents.resetRecordMemoryForTesting(); |
| |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| // Load a page to ensure that at least one draw has happened. |
| doHardwareRenderingSmokeTest(testView); |
| Assert.assertTrue(awContents.hasDrawFunctor()); |
| |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> { |
| var postTask = new FakePostDelayedTask(); |
| awContents.setPostDelayedTaskForTesting(postTask); |
| |
| // Not required to happen in background, but this is how the notification is |
| // dispatched in real code. |
| awContents.onWindowVisibilityChanged(View.INVISIBLE); |
| Assert.assertTrue(awContents.hasDrawFunctor()); |
| Assert.assertEquals(1, postTask.getPendingTasksCount()); |
| |
| awContents.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND); |
| Assert.assertFalse(awContents.hasDrawFunctor()); |
| |
| // Metrics task. |
| var histograms = |
| HistogramWatcher.newBuilder() |
| .expectAnyRecord(AwContents.PSS_HISTOGRAM) |
| .expectAnyRecord(AwContents.PRIVATE_DIRTY_HISTOGRAM) |
| .build(); |
| Assert.assertEquals(2, postTask.getPendingTasksCount()); |
| postTask.fastForwardBy(AwContents.METRICS_COLLECTION_DELAY_MS); |
| Assert.assertEquals(1, postTask.getPendingTasksCount()); |
| histograms.assertExpected(); |
| |
| awContents.onWindowVisibilityChanged(View.VISIBLE); |
| Assert.assertFalse(awContents.hasDrawFunctor()); |
| }); |
| |
| // Rendering still works. |
| doHardwareRenderingSmokeTest(testView, 42, 42, 42); |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> { |
| Assert.assertTrue(awContents.hasDrawFunctor()); |
| }); |
| } |
| |
| @Test |
| @Feature({"AndroidWebView"}) |
| @MediumTest |
| @Features.DisableFeatures({AwFeatures.WEBVIEW_CLEAR_FUNCTOR_IN_BACKGROUND}) |
| public void testMetricsRecordingIsThrottled() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwContents.resetRecordMemoryForTesting(); |
| |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| // Load a page to ensure that at least one draw has happened. |
| doHardwareRenderingSmokeTest(testView); |
| Assert.assertTrue(awContents.hasDrawFunctor()); |
| |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> { |
| var postTask = new FakePostDelayedTask(); |
| awContents.setPostDelayedTaskForTesting(postTask); |
| |
| // Not required to happen in background, but this is how the notification is |
| // dispatched in real code. |
| awContents.onWindowVisibilityChanged(View.INVISIBLE); |
| awContents.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND); |
| Assert.assertTrue(awContents.hasDrawFunctor()); |
| |
| // Metrics task. |
| var histograms = |
| HistogramWatcher.newBuilder() |
| .expectAnyRecord(AwContents.PSS_HISTOGRAM) |
| .expectAnyRecord(AwContents.PRIVATE_DIRTY_HISTOGRAM) |
| .build(); |
| Assert.assertEquals(2, postTask.getPendingTasksCount()); |
| postTask.fastForwardBy(AwContents.METRICS_COLLECTION_DELAY_MS); |
| Assert.assertEquals(1, postTask.getPendingTasksCount()); |
| histograms.assertExpected(); |
| |
| postTask.fastForwardBy( |
| AwContents.FUNCTOR_RECLAIM_DELAY_MS |
| - AwContents.METRICS_COLLECTION_DELAY_MS); |
| Assert.assertEquals(1, postTask.getPendingTasksCount()); |
| |
| // Metrics are not recorded this time, not enough time has passed. |
| histograms = |
| HistogramWatcher.newBuilder() |
| .expectNoRecords(AwContents.PSS_HISTOGRAM) |
| .expectNoRecords(AwContents.PRIVATE_DIRTY_HISTOGRAM) |
| .build(); |
| postTask.fastForwardBy(AwContents.METRICS_COLLECTION_DELAY_MS); |
| Assert.assertEquals(0, postTask.getPendingTasksCount()); |
| histograms.assertExpected(); |
| }); |
| } |
| |
| // Disables hardware acceleration and ensures that there is no crash in the code that adds and |
| // removes frame metrics listener. This code should do nothing when hardware acceleration is |
| // disabled. |
| @Test |
| @DisableHardwareAcceleration |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| @Features.EnableFeatures({BaseFeatures.COLLECT_ANDROID_FRAME_TIMELINE_METRICS}) |
| public void testNoCrashWithoutHardwareAcceleration() throws Throwable { |
| mActivityTestRule.startBrowserProcess(); |
| AwContents.resetRecordMemoryForTesting(); |
| |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| |
| // Frame metrics listener is detached when AwContents becomes invisible. |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> { |
| awContents.onWindowVisibilityChanged(View.INVISIBLE); |
| }); |
| |
| Assert.assertFalse(testView.isBackedByHardwareView()); |
| } |
| |
| private static final class InjectingPlatformServiceBridge extends PlatformServiceBridge { |
| private static final String DEMO_JAVASCRIPT = |
| """ |
| function injectionDemo() { |
| return "injected"; |
| } |
| """; |
| |
| @Override |
| public void injectPlatformJsInterfaces( |
| @NonNull Context context, @NonNull AwContentsWrapper receiver) { |
| receiver.addDocumentStartJavaScript(DEMO_JAVASCRIPT, new String[] {"*"}); |
| } |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| @Features.EnableFeatures({AwFeatures.WEBVIEW_INJECT_PLATFORM_JS_APIS}) |
| public void testPlatformJsInjectedIfEnabled() throws Exception { |
| mActivityTestRule.startBrowserProcess(); |
| |
| InjectingPlatformServiceBridge bridge = new InjectingPlatformServiceBridge(); |
| PlatformServiceBridge.injectInstance(bridge); |
| |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| mActivityTestRule.loadUrlSync( |
| awContents, mContentsClient.getOnPageFinishedHelper(), "about:blank"); |
| |
| String result = |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| awContents, mContentsClient, "injectionDemo();"); |
| Assert.assertEquals("\"injected\"", result); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| @Features.DisableFeatures({AwFeatures.WEBVIEW_INJECT_PLATFORM_JS_APIS}) |
| public void testPlatformJsNotInjectedIfDisabled() throws Exception { |
| mActivityTestRule.startBrowserProcess(); |
| |
| InjectingPlatformServiceBridge bridge = new InjectingPlatformServiceBridge(); |
| PlatformServiceBridge.injectInstance(bridge); |
| |
| AwTestContainerView testView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| final AwContents awContents = testView.getAwContents(); |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| mActivityTestRule.loadUrlSync( |
| awContents, mContentsClient.getOnPageFinishedHelper(), "about:blank"); |
| |
| String result = |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| awContents, |
| mContentsClient, |
| "window.injectionDemo ? 'exists unexpectedly' : 'does not exist';"); |
| Assert.assertEquals("\"does not exist\"", result); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| @SuppressLint("WrongConstant") |
| // crbug.com/1493531 |
| public void testInvalidTouchEventIsRemoved() { |
| mActivityTestRule.startBrowserProcess(); |
| AwContents awContents = |
| mActivityTestRule |
| .createAwTestContainerViewOnMainSync(mContentsClient) |
| .getAwContents(); |
| MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties(); |
| properties.id = 1; |
| properties.toolType = 20; |
| // Create a motion event with one pointer with tool type set to 20. |
| MotionEvent event = |
| MotionEvent.obtain( |
| 0L, |
| 0L, |
| 0, |
| 1, |
| new MotionEvent.PointerProperties[] {properties}, |
| new MotionEvent.PointerCoords[] {new MotionEvent.PointerCoords()}, |
| 0, |
| 0, |
| 0f, |
| 0f, |
| 0, |
| 0, |
| 0, |
| 0); |
| HistogramWatcher watcher = |
| HistogramWatcher.newSingleRecordWatcher("Input.ToolType.Android", 20); |
| Assert.assertFalse(awContents.onTouchEvent(event)); |
| watcher.assertExpected(); |
| } |
| } |