| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // 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 android.net.Uri; |
| import android.webkit.JavascriptInterface; |
| |
| import androidx.test.filters.MediumTest; |
| import androidx.test.filters.SmallTest; |
| |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.ClassRule; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import org.chromium.android_webview.AwContents; |
| import org.chromium.android_webview.JsReplyProxy; |
| import org.chromium.android_webview.ScriptHandler; |
| import org.chromium.android_webview.WebMessageListener; |
| import org.chromium.android_webview.test.TestAwContentsClient.OnReceivedTitleHelper; |
| import org.chromium.android_webview.test.util.CommonResources; |
| import org.chromium.base.test.util.Batch; |
| import org.chromium.base.test.util.Feature; |
| import org.chromium.content_public.browser.MessagePort; |
| import org.chromium.content_public.browser.test.util.TestCallbackHelperContainer.OnPageFinishedHelper; |
| import org.chromium.content_public.browser.test.util.TestThreadUtils; |
| import org.chromium.net.test.EmbeddedTestServer; |
| import org.chromium.net.test.EmbeddedTestServerRule; |
| import org.chromium.net.test.util.TestWebServer; |
| |
| import java.util.concurrent.LinkedBlockingQueue; |
| |
| /** |
| * Test suite for JavaScript Java interaction. |
| */ |
| @RunWith(AwJUnit4ClassRunner.class) |
| @Batch(Batch.PER_CLASS) |
| public class JsJavaInteractionTest { |
| @Rule |
| public AwActivityTestRule mActivityTestRule = new AwActivityTestRule(); |
| @ClassRule |
| public static EmbeddedTestServerRule sTestServerRule = new EmbeddedTestServerRule(); |
| |
| private static final String RESOURCE_PATH = "/android_webview/test/data"; |
| private static final String POST_MESSAGE_SIMPLE_HTML = |
| RESOURCE_PATH + "/post_message_simple.html"; |
| private static final String POST_MESSAGE_WITH_PORTS_HTML = |
| RESOURCE_PATH + "/post_message_with_ports.html"; |
| private static final String POST_MESSAGE_REPEAT_HTML = |
| RESOURCE_PATH + "/post_message_repeat.html"; |
| private static final String POST_MESSAGE_REPLY_HTML = |
| RESOURCE_PATH + "/post_message_receives_reply.html"; |
| private static final String FILE_URI = "file:///android_asset/asset_file.html"; |
| private static final String HELLO_WORLD_HTML = RESOURCE_PATH + "/hello_world.html"; |
| |
| private static final String HELLO = "Hello"; |
| private static final String NEW_TITLE = "new_title"; |
| private static final String JS_OBJECT_NAME = "myObject"; |
| private static final String JS_OBJECT_NAME_2 = "myObject2"; |
| private static final String DATA_HTML = "<html><body>data</body></html>"; |
| private static final int MESSAGE_COUNT = 10000; |
| |
| private EmbeddedTestServer mTestServer; |
| private TestAwContentsClient mContentsClient; |
| private AwContents mAwContents; |
| private TestWebMessageListener mListener; |
| |
| private static class TestWebMessageListener implements WebMessageListener { |
| private LinkedBlockingQueue<Data> mQueue = new LinkedBlockingQueue<>(); |
| |
| public static class Data { |
| public String mMessage; |
| public Uri mSourceOrigin; |
| public boolean mIsMainFrame; |
| public JsReplyProxy mReplyProxy; |
| public MessagePort[] mPorts; |
| |
| public Data(String message, Uri sourceOrigin, boolean isMainFrame, |
| JsReplyProxy replyProxy, MessagePort[] ports) { |
| mMessage = message; |
| mSourceOrigin = sourceOrigin; |
| mIsMainFrame = isMainFrame; |
| mReplyProxy = replyProxy; |
| mPorts = ports; |
| } |
| } |
| |
| @Override |
| public void onPostMessage(String message, Uri sourceOrigin, boolean isMainFrame, |
| JsReplyProxy replyProxy, MessagePort[] ports) { |
| mQueue.add(new Data(message, sourceOrigin, isMainFrame, replyProxy, ports)); |
| } |
| |
| public Data waitForOnPostMessage() throws Exception { |
| return AwActivityTestRule.waitForNextQueueElement(mQueue); |
| } |
| |
| public boolean hasNoMoreOnPostMessage() { |
| return mQueue.isEmpty(); |
| } |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| mContentsClient = new TestAwContentsClient(); |
| final AwTestContainerView testContainerView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| mAwContents = testContainerView.getAwContents(); |
| mListener = new TestWebMessageListener(); |
| mActivityTestRule.getAwSettingsOnUiThread(mAwContents).setJavaScriptEnabled(true); |
| mTestServer = sTestServerRule.getServer(); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testPostMessageSimple() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| final String url = loadUrlFromPath(POST_MESSAGE_SIMPLE_HTML); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| assertUrlHasOrigin(url, data.mSourceOrigin); |
| Assert.assertEquals(HELLO, data.mMessage); |
| Assert.assertTrue(data.mIsMainFrame); |
| Assert.assertEquals(0, data.mPorts.length); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testPostMessage_LoadData_MessageHasStringNullOrigin() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| final String html = "<html><head><script>myObject.postMessage('Hello');</script></head>" |
| + "<body></body></html>"; |
| |
| // This uses loadDataAsync() which is equivalent to WebView#loadData(...). |
| mActivityTestRule.loadHtmlSync( |
| mAwContents, mContentsClient.getOnPageFinishedHelper(), html); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| // Note that the source origin is a non-null string of n, u, l, l. |
| Assert.assertNotNull(data.mSourceOrigin); |
| Assert.assertEquals("null", data.mSourceOrigin.toString()); |
| |
| Assert.assertEquals(HELLO, data.mMessage); |
| Assert.assertTrue(data.mIsMainFrame); |
| Assert.assertEquals(0, data.mPorts.length); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testPostMessageWithPorts() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| final String url = loadUrlFromPath(POST_MESSAGE_WITH_PORTS_HTML); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| final MessagePort[] ports = data.mPorts; |
| assertUrlHasOrigin(url, data.mSourceOrigin); |
| Assert.assertEquals(HELLO, data.mMessage); |
| Assert.assertEquals(1, ports.length); |
| |
| // JavaScript code in the page will change the title to NEW_TITLE if postMessage on |
| // this port succeed. |
| final OnReceivedTitleHelper onReceivedTitleHelper = |
| mContentsClient.getOnReceivedTitleHelper(); |
| final int titleCallCount = onReceivedTitleHelper.getCallCount(); |
| ports[0].postMessage(NEW_TITLE, new MessagePort[0]); |
| onReceivedTitleHelper.waitForCallback(titleCallCount); |
| |
| Assert.assertEquals(NEW_TITLE, onReceivedTitleHelper.getTitle()); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testPostMessageRepeated() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| final String url = loadUrlFromPath(POST_MESSAGE_REPEAT_HTML); |
| for (int i = 0; i < MESSAGE_COUNT; ++i) { |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| assertUrlHasOrigin(url, data.mSourceOrigin); |
| Assert.assertEquals(HELLO + ":" + i, data.mMessage); |
| } |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testPostMessageFromIframeWorks() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| final String frameUrl = mTestServer.getURL(POST_MESSAGE_SIMPLE_HTML); |
| final String html = createCrossOriginAccessTestPageHtml(frameUrl); |
| |
| // Load a cross origin iframe page. |
| mActivityTestRule.loadDataWithBaseUrlSync(mAwContents, |
| mContentsClient.getOnPageFinishedHelper(), html, "text/html", false, |
| "http://www.google.com", null); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| assertUrlHasOrigin(frameUrl, data.mSourceOrigin); |
| Assert.assertEquals(HELLO, data.mMessage); |
| Assert.assertFalse(data.mIsMainFrame); |
| Assert.assertEquals(0, data.mPorts.length); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testAddWebMessageListenerAfterPageLoadWontAffectCurrentPage() throws Throwable { |
| loadUrlFromPath(POST_MESSAGE_SIMPLE_HTML); |
| |
| // Add WebMessageListener after the page loaded won't affect the current page. |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| // Check that we don't have a JavaScript object named JS_OBJECT_NAME |
| Assert.assertFalse(hasJavaScriptObject( |
| JS_OBJECT_NAME, mActivityTestRule, mAwContents, mContentsClient)); |
| |
| // We shouldn't have executed postMessage on JS_OBJECT_NAME either. |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testAddTheSameWebMessageListenerForDifferentJsObjectsWorks() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| addWebMessageListenerOnUiThread( |
| mAwContents, JS_OBJECT_NAME_2, new String[] {"*"}, mListener); |
| |
| loadUrlFromPath(POST_MESSAGE_SIMPLE_HTML); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| Assert.assertEquals(HELLO, data.mMessage); |
| |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| mAwContents, mContentsClient, JS_OBJECT_NAME_2 + ".postMessage('" + HELLO + "');"); |
| |
| TestWebMessageListener.Data data2 = mListener.waitForOnPostMessage(); |
| Assert.assertEquals(HELLO, data2.mMessage); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testFragmentNavigationWontDoJsInjection() throws Throwable { |
| String url = loadUrlFromPath(POST_MESSAGE_SIMPLE_HTML); |
| |
| // Add WebMessageListener after the page loaded won't affect the current page. |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| // Load with fragment url. |
| mActivityTestRule.loadUrlSync( |
| mAwContents, mContentsClient.getOnPageFinishedHelper(), url + "#fragment"); |
| |
| // Check that we don't have a JavaScript object named JS_OBJECT_NAME |
| Assert.assertFalse(hasJavaScriptObject( |
| JS_OBJECT_NAME, mActivityTestRule, mAwContents, mContentsClient)); |
| |
| // We shouldn't have executed postMessage on JS_OBJECT_NAME either. |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testAddWebMessageListenerAffectsRendererInitiatedNavigation() throws Throwable { |
| // TODO(crbug.com/969842): We'd either replace the following html file with a file contains |
| // no JavaScript code or add a test to ensure that evaluateJavascript() won't |
| // over-trigger DidClearWindowObject. |
| loadUrlFromPath(POST_MESSAGE_WITH_PORTS_HTML); |
| |
| // Add WebMessageListener after the page loaded. |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| // Check that we don't have a JavaScript object named JS_OBJECT_NAME |
| Assert.assertFalse(hasJavaScriptObject( |
| JS_OBJECT_NAME, mActivityTestRule, mAwContents, mContentsClient)); |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| |
| // Navigate to a different web page from renderer and wait until the page loading finished. |
| final String url = mTestServer.getURL(POST_MESSAGE_SIMPLE_HTML); |
| final OnPageFinishedHelper onPageFinishedHelper = mContentsClient.getOnPageFinishedHelper(); |
| final int currentCallCount = onPageFinishedHelper.getCallCount(); |
| TestThreadUtils.runOnUiThreadBlocking( |
| () |
| -> mAwContents.evaluateJavaScriptForTests( |
| "window.location.href = '" + url + "';", null)); |
| onPageFinishedHelper.waitForCallback(currentCallCount); |
| |
| // We should expect one onPostMessage event. |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| assertUrlHasOrigin(url, data.mSourceOrigin); |
| Assert.assertEquals(HELLO, data.mMessage); |
| Assert.assertEquals(0, data.mPorts.length); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testAddWebMessageListenerWontAffectOtherAwContents() throws Throwable { |
| // Create another AwContents object. |
| final TestAwContentsClient awContentsClient = new TestAwContentsClient(); |
| final AwTestContainerView awTestContainerView = |
| mActivityTestRule.createAwTestContainerViewOnMainSync(awContentsClient); |
| final AwContents otherAwContents = awTestContainerView.getAwContents(); |
| AwActivityTestRule.enableJavaScriptOnUiThread(otherAwContents); |
| |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| final String url = mTestServer.getURL(POST_MESSAGE_SIMPLE_HTML); |
| mActivityTestRule.loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url); |
| mActivityTestRule.loadUrlSync( |
| otherAwContents, awContentsClient.getOnPageFinishedHelper(), url); |
| |
| // Verify that WebMessageListener was set successfually on mAwContents. |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| assertUrlHasOrigin(url, data.mSourceOrigin); |
| Assert.assertEquals(HELLO, data.mMessage); |
| Assert.assertEquals(0, data.mPorts.length); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| |
| // Verify that we don't have myObject injected to otherAwContents. |
| Assert.assertFalse(hasJavaScriptObject( |
| JS_OBJECT_NAME, mActivityTestRule, otherAwContents, awContentsClient)); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testAddWebMessageListenerAllowsCertainUrlWorksWithIframe() throws Throwable { |
| final String frameUrl = mTestServer.getURL(POST_MESSAGE_SIMPLE_HTML); |
| final String html = createCrossOriginAccessTestPageHtml(frameUrl); |
| addWebMessageListenerOnUiThread( |
| mAwContents, JS_OBJECT_NAME, new String[] {parseOrigin(frameUrl)}, mListener); |
| |
| mActivityTestRule.loadDataWithBaseUrlSync(mAwContents, |
| mContentsClient.getOnPageFinishedHelper(), html, "text/html", false, |
| "http://www.google.com", null); |
| |
| // The iframe should have myObject injected. |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| Assert.assertEquals(HELLO, data.mMessage); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| |
| // Verify that the main frame has no myObject injected. |
| Assert.assertFalse(hasJavaScriptObject( |
| JS_OBJECT_NAME, mActivityTestRule, mAwContents, mContentsClient)); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testRemoveWebMessageListener_preventInjectionForNextPageLoad() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| // Load the the page. |
| loadUrlFromPath(POST_MESSAGE_SIMPLE_HTML); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| Assert.assertEquals(HELLO, data.mMessage); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| |
| // Remove WebMessageListener will disable injection for next page load. |
| removeWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME); |
| |
| loadUrlFromPath(POST_MESSAGE_SIMPLE_HTML); |
| |
| // Should have no myObject injected. |
| Assert.assertFalse(hasJavaScriptObject( |
| JS_OBJECT_NAME, mActivityTestRule, mAwContents, mContentsClient)); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testRemoveWebMessageListener_cutJsJavaMappingImmediately() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| // Load the the page. |
| loadUrlFromPath(POST_MESSAGE_SIMPLE_HTML); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| Assert.assertEquals(HELLO, data.mMessage); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| |
| // Remove WebMessageListener will disable injection for next page load. |
| removeWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME); |
| |
| // Should still have myObject. |
| Assert.assertTrue(hasJavaScriptObject( |
| JS_OBJECT_NAME, mActivityTestRule, mAwContents, mContentsClient)); |
| |
| // But posting message on myObject will be dropped. |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| mAwContents, mContentsClient, JS_OBJECT_NAME + ".postMessage('" + HELLO + "');"); |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testRemoveWebMessageListener_removeWithNoAddWebMessageListener() throws Throwable { |
| // Call removeWebMessageListener() without addWebMessageListener() shouldn't fail. |
| removeWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME); |
| |
| loadUrlFromPath(POST_MESSAGE_SIMPLE_HTML); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testRemoveWebMessageListener_removeBeforeLoadPage() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| removeWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME); |
| |
| loadUrlFromPath(POST_MESSAGE_SIMPLE_HTML); |
| |
| Assert.assertFalse(hasJavaScriptObject( |
| JS_OBJECT_NAME, mActivityTestRule, mAwContents, mContentsClient)); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testRemoveWebMessageListener_extraRemove() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| removeWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME); |
| // Extra removeWebMessageListener() does nothing. |
| removeWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME); |
| |
| loadUrlFromPath(POST_MESSAGE_SIMPLE_HTML); |
| |
| Assert.assertFalse(hasJavaScriptObject( |
| JS_OBJECT_NAME, mActivityTestRule, mAwContents, mContentsClient)); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testAllowedOriginsWorksForVariousBaseUrls() throws Throwable { |
| // Set a typical rule. |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, |
| new String[] {"https://www.example.com:443"}, mListener); |
| |
| Assert.assertTrue( |
| isJsObjectInjectedWhenLoadingUrl("https://www.example.com", JS_OBJECT_NAME)); |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.example.com:8080", JS_OBJECT_NAME)); |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("http://www.example.com", JS_OBJECT_NAME)); |
| Assert.assertFalse(isJsObjectInjectedWhenLoadingUrl("http://example.com", JS_OBJECT_NAME)); |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.google.com", JS_OBJECT_NAME)); |
| Assert.assertFalse(isJsObjectInjectedWhenLoadingUrl("file://etc", JS_OBJECT_NAME)); |
| Assert.assertFalse(isJsObjectInjectedWhenLoadingUrl("ftp://example.com", JS_OBJECT_NAME)); |
| Assert.assertFalse(isJsObjectInjectedWhenLoadingUrl(null, JS_OBJECT_NAME)); |
| |
| // Inject to all frames. |
| addWebMessageListenerOnUiThread( |
| mAwContents, JS_OBJECT_NAME_2, new String[] {"*"}, mListener); |
| |
| Assert.assertTrue( |
| isJsObjectInjectedWhenLoadingUrl("https://www.example.com", JS_OBJECT_NAME_2)); |
| Assert.assertTrue( |
| isJsObjectInjectedWhenLoadingUrl("https://www.example.com:8080", JS_OBJECT_NAME_2)); |
| Assert.assertTrue( |
| isJsObjectInjectedWhenLoadingUrl("http://www.example.com", JS_OBJECT_NAME_2)); |
| Assert.assertTrue(isJsObjectInjectedWhenLoadingUrl("http://example.com", JS_OBJECT_NAME_2)); |
| Assert.assertTrue( |
| isJsObjectInjectedWhenLoadingUrl("https://www.google.com", JS_OBJECT_NAME_2)); |
| Assert.assertTrue(isJsObjectInjectedWhenLoadingUrl("file://etc", JS_OBJECT_NAME_2)); |
| Assert.assertTrue(isJsObjectInjectedWhenLoadingUrl("ftp://example.com", JS_OBJECT_NAME_2)); |
| Assert.assertTrue(isJsObjectInjectedWhenLoadingUrl(null, JS_OBJECT_NAME_2)); |
| |
| // WebView doesn't support ftp with loadUrl() but ftp scheme could happen with |
| // loadDataWithBaseUrl(). |
| final String jsObjectName3 = JS_OBJECT_NAME + "3"; |
| addWebMessageListenerOnUiThread( |
| mAwContents, jsObjectName3, new String[] {"ftp://"}, mListener); |
| // ftp is a standard scheme, so the origin will be "ftp://example.com", however we don't |
| // support host rule for ftp://, so it won't do the injection. |
| Assert.assertFalse(isJsObjectInjectedWhenLoadingUrl("ftp://example.com", jsObjectName3)); |
| |
| // file scheme. |
| final String jsObjectName4 = JS_OBJECT_NAME + "4"; |
| addWebMessageListenerOnUiThread( |
| mAwContents, jsObjectName4, new String[] {"file://"}, mListener); |
| Assert.assertTrue(isJsObjectInjectedWhenLoadingUrl("file://etc", jsObjectName4)); |
| |
| // Pass an URI instead of origin shouldn't work. |
| final String jsObjectName5 = JS_OBJECT_NAME + "5"; |
| try { |
| addWebMessageListenerOnUiThread(mAwContents, jsObjectName5, |
| new String[] {"https://www.example.com/index.html"}, mListener); |
| Assert.fail("allowedOriginRules shouldn't be url like"); |
| } catch (RuntimeException e) { |
| // Should catch IllegalArgumentException in the end of the re-throw chain. |
| Assert.assertTrue(getRootCauseException(e) instanceof IllegalArgumentException); |
| } |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.example.com", jsObjectName5)); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testDontAllowAddWebMessageLitenerWithTheSameJsObjectName() { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| try { |
| addWebMessageListenerOnUiThread( |
| mAwContents, JS_OBJECT_NAME, new String[] {"*"}, new TestWebMessageListener()); |
| Assert.fail("Shouldn't allow the same Js object name be added more than once."); |
| } catch (RuntimeException e) { |
| // Should catch IllegalArgumentException in the end of the re-throw chain. |
| Assert.assertTrue(getRootCauseException(e) instanceof IllegalArgumentException); |
| } |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testAddWebMessageListener_SameOrigins() throws Throwable { |
| final String[] allowedOriginRules = |
| new String[] {"https://www.example.com", "https://www.allowed.com"}; |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, allowedOriginRules, mListener); |
| |
| Assert.assertTrue( |
| isJsObjectInjectedWhenLoadingUrl("https://www.example.com", JS_OBJECT_NAME)); |
| Assert.assertTrue( |
| isJsObjectInjectedWhenLoadingUrl("https://www.allowed.com", JS_OBJECT_NAME)); |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.noinjection.com", JS_OBJECT_NAME)); |
| |
| // Call addWebMessageListener() with the same set of origins. |
| addWebMessageListenerOnUiThread( |
| mAwContents, JS_OBJECT_NAME_2, allowedOriginRules, mListener); |
| |
| Assert.assertTrue( |
| isJsObjectInjectedWhenLoadingUrl("https://www.example.com", JS_OBJECT_NAME_2)); |
| Assert.assertTrue( |
| isJsObjectInjectedWhenLoadingUrl("https://www.allowed.com", JS_OBJECT_NAME_2)); |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.noinjection.com", JS_OBJECT_NAME_2)); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testAddWebMessageListener_OverlappingSetOfOrigins() throws Throwable { |
| final String[] allowedOriginRules1 = |
| new String[] {"https://www.example.com", "https://www.allowed.com"}; |
| addWebMessageListenerOnUiThread( |
| mAwContents, JS_OBJECT_NAME, allowedOriginRules1, mListener); |
| |
| Assert.assertTrue( |
| isJsObjectInjectedWhenLoadingUrl("https://www.example.com", JS_OBJECT_NAME)); |
| Assert.assertTrue( |
| isJsObjectInjectedWhenLoadingUrl("https://www.allowed.com", JS_OBJECT_NAME)); |
| Assert.assertFalse(isJsObjectInjectedWhenLoadingUrl("https://www.ok.com", JS_OBJECT_NAME)); |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.noinjection.com", JS_OBJECT_NAME)); |
| |
| final String[] allowedOriginRules2 = |
| new String[] {"https://www.example.com", "https://www.ok.com"}; |
| // Call addWebMessageListener with overlapping set of origins. |
| addWebMessageListenerOnUiThread( |
| mAwContents, JS_OBJECT_NAME_2, allowedOriginRules2, mListener); |
| |
| Assert.assertTrue( |
| isJsObjectInjectedWhenLoadingUrl("https://www.example.com", JS_OBJECT_NAME_2)); |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.allowed.com", JS_OBJECT_NAME_2)); |
| Assert.assertTrue(isJsObjectInjectedWhenLoadingUrl("https://www.ok.com", JS_OBJECT_NAME_2)); |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.noinjection.com", JS_OBJECT_NAME_2)); |
| |
| // Remove the listener should remove the js object from the next navigation. |
| removeWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME_2); |
| |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.example.com", JS_OBJECT_NAME_2)); |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.allowed.com", JS_OBJECT_NAME_2)); |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.ok.com", JS_OBJECT_NAME_2)); |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.noinjection.com", JS_OBJECT_NAME_2)); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testAddWebMessageListener_NonOverlappingSetOfOrigins() throws Throwable { |
| final String[] allowedOriginRules1 = |
| new String[] {"https://www.example.com", "https://www.allowed.com"}; |
| addWebMessageListenerOnUiThread( |
| mAwContents, JS_OBJECT_NAME, allowedOriginRules1, mListener); |
| |
| Assert.assertTrue( |
| isJsObjectInjectedWhenLoadingUrl("https://www.example.com", JS_OBJECT_NAME)); |
| Assert.assertTrue( |
| isJsObjectInjectedWhenLoadingUrl("https://www.allowed.com", JS_OBJECT_NAME)); |
| Assert.assertFalse(isJsObjectInjectedWhenLoadingUrl("https://www.ok.com", JS_OBJECT_NAME)); |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.noinjection.com", JS_OBJECT_NAME)); |
| |
| final String[] allowedOriginRules2 = new String[] {"https://www.ok.com"}; |
| // Call addWebMessageListener with non-overlapping set of origins. |
| addWebMessageListenerOnUiThread( |
| mAwContents, JS_OBJECT_NAME_2, allowedOriginRules2, mListener); |
| |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.example.com", JS_OBJECT_NAME_2)); |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.allowed.com", JS_OBJECT_NAME_2)); |
| Assert.assertTrue(isJsObjectInjectedWhenLoadingUrl("https://www.ok.com", JS_OBJECT_NAME_2)); |
| Assert.assertFalse( |
| isJsObjectInjectedWhenLoadingUrl("https://www.noinjection.com", JS_OBJECT_NAME_2)); |
| Assert.assertFalse(isJsObjectInjectedWhenLoadingUrl("", JS_OBJECT_NAME_2)); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testJsReplyProxyWorks() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| final String url = loadUrlFromPath(POST_MESSAGE_REPLY_HTML); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| // JavaScript code in the page will change the title to NEW_TITLE if postMessage to proxy |
| // succeed. |
| final OnReceivedTitleHelper onReceivedTitleHelper = |
| mContentsClient.getOnReceivedTitleHelper(); |
| final int titleCallCount = onReceivedTitleHelper.getCallCount(); |
| data.mReplyProxy.postMessage(NEW_TITLE); |
| onReceivedTitleHelper.waitForCallback(titleCallCount); |
| |
| Assert.assertEquals(NEW_TITLE, onReceivedTitleHelper.getTitle()); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testJsReplyProxyReplyToTheCorrectJsObject() throws Throwable { |
| final TestWebMessageListener webMessageListener2 = new TestWebMessageListener(); |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| addWebMessageListenerOnUiThread( |
| mAwContents, JS_OBJECT_NAME_2, new String[] {"*"}, webMessageListener2); |
| |
| final String url = loadUrlFromPath(POST_MESSAGE_REPLY_HTML); |
| |
| // Listener for myObject. |
| final String listener1 = "function (event) {" |
| + " " + JS_OBJECT_NAME + ".postMessage('ack1' + event.data);" |
| + "}"; |
| |
| // Listener for myObject2. |
| final String listener2 = "function (event) {" |
| + " " + JS_OBJECT_NAME_2 + ".postMessage('ack2' + event.data);" |
| + "}"; |
| |
| // Add two different js objects. |
| addEventListener(listener1, "listener1", JS_OBJECT_NAME, mActivityTestRule, mAwContents, |
| mContentsClient); |
| addEventListener(listener2, "listener2", JS_OBJECT_NAME_2, mActivityTestRule, mAwContents, |
| mContentsClient); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| final String message = "message"; |
| mActivityTestRule.executeJavaScriptAndWaitForResult(mAwContents, mContentsClient, |
| JS_OBJECT_NAME_2 + ".postMessage('" + message + "');"); |
| TestWebMessageListener.Data data2 = webMessageListener2.waitForOnPostMessage(); |
| |
| Assert.assertEquals(message, data2.mMessage); |
| |
| // Targeting myObject. |
| data.mReplyProxy.postMessage(HELLO); |
| // Targeting myObject2. |
| data2.mReplyProxy.postMessage(HELLO); |
| |
| TestWebMessageListener.Data replyData1 = mListener.waitForOnPostMessage(); |
| TestWebMessageListener.Data replyData2 = webMessageListener2.waitForOnPostMessage(); |
| |
| Assert.assertEquals("ack1" + HELLO, replyData1.mMessage); |
| Assert.assertEquals("ack2" + HELLO, replyData2.mMessage); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| Assert.assertTrue(webMessageListener2.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testJsReplyProxyDropsMessageIfJsObjectIsGone() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| final String url = loadUrlFromPath(POST_MESSAGE_REPLY_HTML); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| JsReplyProxy proxy = data.mReplyProxy; |
| |
| // Load the same url again. |
| loadUrlFromPath(POST_MESSAGE_REPLY_HTML); |
| TestWebMessageListener.Data data2 = mListener.waitForOnPostMessage(); |
| |
| // Use the previous JsReplyProxy to send message. It should drop the message. |
| proxy.postMessage(NEW_TITLE); |
| |
| // Call evaluateJavascript to make sure the previous postMessage() call is reached to |
| // renderer if it should, since these messages are in sequence. |
| Assert.assertTrue(hasJavaScriptObject( |
| JS_OBJECT_NAME, mActivityTestRule, mAwContents, mContentsClient)); |
| |
| // Title shouldn't change. |
| Assert.assertNotEquals(NEW_TITLE, mActivityTestRule.getTitleOnUiThread(mAwContents)); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testJsAddAndRemoveEventListener() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| loadUrlFromPath(POST_MESSAGE_SIMPLE_HTML); |
| |
| JsReplyProxy proxy = mListener.waitForOnPostMessage().mReplyProxy; |
| |
| final String listener1 = "function (event) {" |
| + " if (window.receivedCount1) {" |
| + " window.receivedCount1++;" |
| + " } else {" |
| + " window.receivedCount1 = 1;" |
| + " }" |
| + " " + JS_OBJECT_NAME + ".postMessage('ack1:' + window.receivedCount1);" |
| + "}"; |
| |
| final String listener2 = "function (event) {" |
| + " if (window.receivedCount2) {" |
| + " window.receivedCount2++;" |
| + " } else {" |
| + " window.receivedCount2 = 1;" |
| + " }" |
| + " " + JS_OBJECT_NAME + ".postMessage('ack2:' + window.receivedCount2);" |
| + "}"; |
| |
| addEventListener(listener1, "listener1", JS_OBJECT_NAME, mActivityTestRule, mAwContents, |
| mContentsClient); |
| addEventListener(listener2, "listener2", JS_OBJECT_NAME, mActivityTestRule, mAwContents, |
| mContentsClient); |
| |
| // Post message to test both listeners receive message. |
| proxy.postMessage(HELLO); |
| |
| TestWebMessageListener.Data replyData1 = mListener.waitForOnPostMessage(); |
| TestWebMessageListener.Data replyData2 = mListener.waitForOnPostMessage(); |
| |
| Assert.assertEquals("ack1:1", replyData1.mMessage); |
| Assert.assertEquals("ack2:1", replyData2.mMessage); |
| |
| removeEventListener( |
| "listener2", JS_OBJECT_NAME, mActivityTestRule, mAwContents, mContentsClient); |
| |
| // Post message again to test if remove works. |
| proxy.postMessage(HELLO); |
| |
| // listener 1 should add message again. |
| TestWebMessageListener.Data replyData3 = mListener.waitForOnPostMessage(); |
| Assert.assertEquals("ack1:2", replyData3.mMessage); |
| |
| // Should be no more messages. |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testJsObjectRemoveOnMessage() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| final String url = loadUrlFromPath(POST_MESSAGE_REPLY_HTML); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| // JavaScript code in the page will change the title to NEW_TITLE if postMessage to proxy |
| // succeed. |
| final OnReceivedTitleHelper onReceivedTitleHelper = |
| mContentsClient.getOnReceivedTitleHelper(); |
| final int titleCallCount = onReceivedTitleHelper.getCallCount(); |
| data.mReplyProxy.postMessage(NEW_TITLE); |
| onReceivedTitleHelper.waitForCallback(titleCallCount); |
| |
| Assert.assertEquals(NEW_TITLE, onReceivedTitleHelper.getTitle()); |
| |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| mAwContents, mContentsClient, JS_OBJECT_NAME + ".onmessage = undefined;"); |
| Assert.assertEquals("null", |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| mAwContents, mContentsClient, JS_OBJECT_NAME + ".onmessage")); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| private void verifyOnPostMessageOriginIsNull() throws Throwable { |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| mAwContents, mContentsClient, JS_OBJECT_NAME + ".postMessage('Hello');"); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| Assert.assertEquals("null", data.mSourceOrigin.toString()); |
| |
| Assert.assertEquals(HELLO, data.mMessage); |
| Assert.assertTrue(data.mIsMainFrame); |
| Assert.assertEquals(0, data.mPorts.length); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testFileSchemeUrl_setAllowFileAccessFromFile_true() throws Throwable { |
| mAwContents.getSettings().setAllowFileAccessFromFileURLs(true); |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| mActivityTestRule.loadUrlSync( |
| mAwContents, mContentsClient.getOnPageFinishedHelper(), FILE_URI); |
| Assert.assertEquals("\"file://\"", |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| mAwContents, mContentsClient, "window.origin")); |
| |
| verifyOnPostMessageOriginIsNull(); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testFileSchemeUrl_setAllowFileAccessFromFile_false() throws Throwable { |
| // The default value is false on JELLY_BEAN and above, but we explicitly set this to |
| // false to readability. |
| mAwContents.getSettings().setAllowFileAccessFromFileURLs(false); |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| mActivityTestRule.loadUrlSync( |
| mAwContents, mContentsClient.getOnPageFinishedHelper(), FILE_URI); |
| Assert.assertEquals("\"null\"", |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| mAwContents, mContentsClient, "window.origin")); |
| |
| verifyOnPostMessageOriginIsNull(); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testContentSchemeUrl_setAllowFileAccessFromFileURLs_true() throws Throwable { |
| mAwContents.getSettings().setAllowContentAccess(true); |
| mAwContents.getSettings().setAllowFileAccessFromFileURLs(true); |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| mActivityTestRule.loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), |
| TestContentProvider.createContentUrl("content_access")); |
| Assert.assertEquals("\"content://\"", |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| mAwContents, mContentsClient, "window.origin")); |
| |
| verifyOnPostMessageOriginIsNull(); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testContentSchemeUrl_setAllowFileAccessFromFileURLs_false() throws Throwable { |
| mAwContents.getSettings().setAllowContentAccess(true); |
| // The default value is false on JELLY_BEAN and above, but we explicitly set this to |
| // false to readability. |
| mAwContents.getSettings().setAllowFileAccessFromFileURLs(false); |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| mActivityTestRule.loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), |
| TestContentProvider.createContentUrl("content_access")); |
| Assert.assertEquals("\"null\"", |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| mAwContents, mContentsClient, "window.origin")); |
| |
| verifyOnPostMessageOriginIsNull(); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testWebMessageListenerForPopupWindow() throws Throwable { |
| TestWebServer webServer = TestWebServer.start(); |
| |
| final String popupPath = "/popup.html"; |
| final String parentPageHtml = CommonResources.makeHtmlPageFrom("", |
| "<script>" |
| + "function tryOpenWindow() {" |
| + " var newWindow = window.open('" + popupPath + "');" |
| + "}</script>"); |
| |
| final String popupPageHtml = |
| CommonResources.makeHtmlPageFrom("<title>popup</title>", "This is a popup window"); |
| |
| mActivityTestRule.triggerPopup(mAwContents, mContentsClient, webServer, parentPageHtml, |
| popupPageHtml, popupPath, "tryOpenWindow()"); |
| AwActivityTestRule.PopupInfo popupInfo = mActivityTestRule.createPopupContents(mAwContents); |
| TestAwContentsClient popupContentsClient = popupInfo.popupContentsClient; |
| final AwContents popupContents = popupInfo.popupContents; |
| |
| // App adds WebMessageListener to the child WebView, e.g. in onCreateWindow(). |
| addWebMessageListenerOnUiThread( |
| popupContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| mActivityTestRule.loadPopupContents(mAwContents, popupInfo, null); |
| |
| // Test if the listener was re-injected. |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| popupContents, popupContentsClient, JS_OBJECT_NAME + ".postMessage('Hello');"); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| Assert.assertEquals(HELLO, data.mMessage); |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| |
| webServer.shutdown(); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testPostMessage_JsObjectName_Number() throws Throwable { |
| checkInjectAndAccessJsObjectNameAsWindowProperty("123"); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testPostMessage_JsObjectName_Symbol() throws Throwable { |
| checkInjectAndAccessJsObjectNameAsWindowProperty("*"); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testPostMessage_JsObjectName_Keyword() throws Throwable { |
| checkInjectAndAccessJsObjectNameAsWindowProperty("var"); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testDocumentStartJavaScript_addJavascriptInterfaceShouldBeAvaliable() |
| throws Throwable { |
| final LinkedBlockingQueue<String> javascriptInterfaceQueue = new LinkedBlockingQueue<>(); |
| AwActivityTestRule.addJavascriptInterfaceOnUiThread(mAwContents, new Object() { |
| @JavascriptInterface |
| public void send(String message) { |
| javascriptInterfaceQueue.add(message); |
| } |
| }, "javaBridge"); |
| addDocumentStartJavaScriptOnUiThread( |
| mAwContents, "javaBridge.send('" + HELLO + "');", new String[] {"*"}); |
| |
| loadUrlFromPath(HELLO_WORLD_HTML); |
| |
| String message = AwActivityTestRule.waitForNextQueueElement(javascriptInterfaceQueue); |
| |
| Assert.assertEquals(HELLO, message); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testDocumentStartJavaScript_jsObjectShouldBeAvaliable() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| addDocumentStartJavaScriptOnUiThread( |
| mAwContents, JS_OBJECT_NAME + ".postMessage('" + HELLO + "');", new String[] {"*"}); |
| |
| String url = loadUrlFromPath(HELLO_WORLD_HTML); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| assertUrlHasOrigin(url, data.mSourceOrigin); |
| Assert.assertEquals(HELLO, data.mMessage); |
| Assert.assertTrue(data.mIsMainFrame); |
| Assert.assertEquals(0, data.mPorts.length); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testDocumentStartJavaScript_runBeforeUserScript() throws Throwable { |
| addDocumentStartJavaScriptOnUiThread(mAwContents, |
| JS_OBJECT_NAME + ".postMessage('" + HELLO + "1');", new String[] {"*"}); |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| // POST_MESSAGE_SIMPLE_HTML will post HELLO message. |
| String url = loadUrlFromPath(POST_MESSAGE_SIMPLE_HTML); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| assertUrlHasOrigin(url, data.mSourceOrigin); |
| Assert.assertEquals(HELLO + "1", data.mMessage); |
| Assert.assertTrue(data.mIsMainFrame); |
| Assert.assertEquals(0, data.mPorts.length); |
| |
| TestWebMessageListener.Data data2 = mListener.waitForOnPostMessage(); |
| Assert.assertEquals(HELLO, data2.mMessage); |
| Assert.assertTrue(data2.mIsMainFrame); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testDocumentStartJavaScript_multipleScripts() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| addDocumentStartJavaScriptOnUiThread(mAwContents, |
| JS_OBJECT_NAME + ".postMessage('" + HELLO + "0');", new String[] {"*"}); |
| addDocumentStartJavaScriptOnUiThread(mAwContents, |
| JS_OBJECT_NAME + ".postMessage('" + HELLO + "1');", new String[] {"*"}); |
| |
| String url = loadUrlFromPath(HELLO_WORLD_HTML); |
| |
| for (int i = 0; i < 2; ++i) { |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| assertUrlHasOrigin(url, data.mSourceOrigin); |
| Assert.assertEquals(HELLO + Integer.toString(i), data.mMessage); |
| Assert.assertTrue(data.mIsMainFrame); |
| Assert.assertEquals(0, data.mPorts.length); |
| } |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testDocumentStartJavaScript_callAgainAfterPageLoad() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| addDocumentStartJavaScriptOnUiThread(mAwContents, |
| JS_OBJECT_NAME + ".postMessage('" + HELLO + "0');", new String[] {"*"}); |
| |
| String url = loadUrlFromPath(HELLO_WORLD_HTML); |
| |
| addDocumentStartJavaScriptOnUiThread(mAwContents, |
| JS_OBJECT_NAME + ".postMessage('" + HELLO + "1');", new String[] {"*"}); |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| assertUrlHasOrigin(url, data.mSourceOrigin); |
| Assert.assertEquals(HELLO + "0", data.mMessage); |
| Assert.assertTrue(data.mIsMainFrame); |
| Assert.assertEquals(0, data.mPorts.length); |
| |
| // Load the page again. |
| loadUrlFromPath(HELLO_WORLD_HTML); |
| |
| for (int i = 0; i < 2; ++i) { |
| TestWebMessageListener.Data data2 = mListener.waitForOnPostMessage(); |
| |
| Assert.assertEquals(HELLO + Integer.toString(i), data2.mMessage); |
| Assert.assertTrue(data2.mIsMainFrame); |
| } |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testDocumentStartJavaScript_allowedOriginsRulesWithDifferentBaseUrl() |
| throws Throwable { |
| // With a standard origin rule. |
| final String testObjectName = "test"; |
| addDocumentStartJavaScriptOnUiThread(mAwContents, "let " + testObjectName + " = {};", |
| new String[] {"https://www.example.com:443"}); |
| |
| Assert.assertTrue(didScriptRunWhenLoading("https://www.example.com", testObjectName)); |
| Assert.assertFalse(didScriptRunWhenLoading("https://www.example.com:8080", testObjectName)); |
| Assert.assertFalse(didScriptRunWhenLoading("http://www.example.com", testObjectName)); |
| Assert.assertFalse(didScriptRunWhenLoading("http://example.com", testObjectName)); |
| Assert.assertFalse(didScriptRunWhenLoading("https://www.google.com", testObjectName)); |
| Assert.assertFalse(didScriptRunWhenLoading("file://etc", testObjectName)); |
| Assert.assertFalse(didScriptRunWhenLoading("ftp://example.com", testObjectName)); |
| Assert.assertFalse(didScriptRunWhenLoading(null, testObjectName)); |
| |
| // Match all the origins. |
| final String testObjectName2 = testObjectName + "2"; |
| addDocumentStartJavaScriptOnUiThread( |
| mAwContents, "let " + testObjectName2 + " = {};", new String[] {"*"}); |
| |
| Assert.assertTrue(didScriptRunWhenLoading("https://www.example.com", testObjectName2)); |
| Assert.assertTrue(didScriptRunWhenLoading("https://www.example.com:8080", testObjectName2)); |
| Assert.assertTrue(didScriptRunWhenLoading("http://www.example.com", testObjectName2)); |
| Assert.assertTrue(didScriptRunWhenLoading("http://example.com", testObjectName2)); |
| Assert.assertTrue(didScriptRunWhenLoading("https://www.google.com", testObjectName2)); |
| Assert.assertTrue(didScriptRunWhenLoading("file://etc", testObjectName2)); |
| Assert.assertTrue(didScriptRunWhenLoading("ftp://example.com", testObjectName2)); |
| Assert.assertTrue(didScriptRunWhenLoading(null, testObjectName2)); |
| // data: scheme could be matched with "*". |
| final String html = "<html><body><div>data</div></body></html>"; |
| mActivityTestRule.loadHtmlSync( |
| mAwContents, mContentsClient.getOnPageFinishedHelper(), html); |
| Assert.assertTrue(hasJavaScriptObject( |
| testObjectName2, mActivityTestRule, mAwContents, mContentsClient)); |
| |
| // Wrong origin rule. |
| final String testObjectName5 = testObjectName + "5"; |
| try { |
| addDocumentStartJavaScriptOnUiThread(mAwContents, "let " + testObjectName5 + " = {};", |
| new String[] {"https://www.example.com/index.html"}); |
| Assert.fail("You cannot use a full URL for allowedOriginRules."); |
| } catch (RuntimeException e) { |
| // Should catch IllegalArgumentException in the end of the re-throw chain. |
| Assert.assertTrue("The exception should be an IllegalArgumentException", |
| getRootCauseException(e) instanceof IllegalArgumentException); |
| } |
| Assert.assertFalse(didScriptRunWhenLoading("https://www.example.com", testObjectName5)); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testDocumentStartJavaScript_willRunInIframe() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| final String script = "if (window.location.origin !== 'http://www.google.com') {" |
| + " " + JS_OBJECT_NAME + ".postMessage('" + HELLO + "');" |
| + "}"; |
| // Since we are matching both origins, the script will run in both iframe and main frame, |
| // but it will send message in only iframe. |
| addDocumentStartJavaScriptOnUiThread(mAwContents, script, new String[] {"*"}); |
| |
| final String frameUrl = mTestServer.getURL(HELLO_WORLD_HTML); |
| final String html = createCrossOriginAccessTestPageHtml(frameUrl); |
| |
| // Load a cross origin iframe page, the www.google.com page is the main frame, test server |
| // page is the iframe. |
| mActivityTestRule.loadDataWithBaseUrlSync(mAwContents, |
| mContentsClient.getOnPageFinishedHelper(), html, "text/html", false, |
| "http://www.google.com", null); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| assertUrlHasOrigin(frameUrl, data.mSourceOrigin); |
| Assert.assertEquals(HELLO, data.mMessage); |
| Assert.assertFalse(data.mIsMainFrame); |
| Assert.assertEquals(0, data.mPorts.length); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testDocumentStartJavaScript_removeScript() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| ScriptHandler[] handlers = new ScriptHandler[2]; |
| for (int i = 0; i < 2; ++i) { |
| final String script = |
| JS_OBJECT_NAME + ".postMessage('" + HELLO + Integer.toString(i) + "');"; |
| // Since we are matching both origins, the script will run in both iframe and main |
| // frame, but it will send message in only iframe. |
| handlers[i] = |
| addDocumentStartJavaScriptOnUiThread(mAwContents, script, new String[] {"*"}); |
| } |
| |
| final String url = loadUrlFromPath(HELLO_WORLD_HTML); |
| |
| for (int i = 0; i < 2; ++i) { |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| assertUrlHasOrigin(url, data.mSourceOrigin); |
| Assert.assertEquals(HELLO + Integer.toString(i), data.mMessage); |
| } |
| |
| TestThreadUtils.runOnUiThreadBlocking(() -> handlers[0].remove()); |
| // Load the page again. |
| loadUrlFromPath(HELLO_WORLD_HTML); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| Assert.assertEquals(HELLO + "1", data.mMessage); |
| |
| TestThreadUtils.runOnUiThreadBlocking(() -> handlers[1].remove()); |
| // Load the page again. |
| loadUrlFromPath(HELLO_WORLD_HTML); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView", "JsJavaInteraction"}) |
| public void testDocumentStartJavaScript_doubleRemoveScript() throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, JS_OBJECT_NAME, new String[] {"*"}, mListener); |
| |
| final String script = JS_OBJECT_NAME + ".postMessage('" + HELLO + "');"; |
| ScriptHandler handler = |
| addDocumentStartJavaScriptOnUiThread(mAwContents, script, new String[] {"*"}); |
| |
| final String url = loadUrlFromPath(HELLO_WORLD_HTML); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| assertUrlHasOrigin(url, data.mSourceOrigin); |
| Assert.assertEquals(HELLO, data.mMessage); |
| |
| // Remove twice, the second time should take no effect. |
| TestThreadUtils.runOnUiThreadBlocking(() -> handler.remove()); |
| TestThreadUtils.runOnUiThreadBlocking(() -> handler.remove()); |
| // Load the page again. |
| loadUrlFromPath(HELLO_WORLD_HTML); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| |
| // Remove twice again, should have no effect. |
| TestThreadUtils.runOnUiThreadBlocking(() -> handler.remove()); |
| TestThreadUtils.runOnUiThreadBlocking(() -> handler.remove()); |
| // Load the page again. |
| loadUrlFromPath(HELLO_WORLD_HTML); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| private void checkInjectAndAccessJsObjectNameAsWindowProperty(String jsObjName) |
| throws Throwable { |
| addWebMessageListenerOnUiThread(mAwContents, jsObjName, new String[] {"*"}, mListener); |
| |
| String html = "<html><head><script>window['" + jsObjName + "'].postMessage('Hello');" |
| + "</script></head><body><div>postMessage</div></body></html>"; |
| mActivityTestRule.loadDataWithBaseUrlSync(mAwContents, |
| mContentsClient.getOnPageFinishedHelper(), html, "text/html", false, |
| "http://www.google.com", null); |
| |
| TestWebMessageListener.Data data = mListener.waitForOnPostMessage(); |
| |
| Assert.assertEquals("http://www.google.com", data.mSourceOrigin.toString()); |
| Assert.assertEquals(HELLO, data.mMessage); |
| Assert.assertTrue(data.mIsMainFrame); |
| Assert.assertEquals(0, data.mPorts.length); |
| |
| Assert.assertTrue(mListener.hasNoMoreOnPostMessage()); |
| } |
| |
| private boolean isJsObjectInjectedWhenLoadingUrl( |
| final String baseUrl, final String jsObjectName) throws Throwable { |
| mActivityTestRule.loadDataWithBaseUrlSync(mAwContents, |
| mContentsClient.getOnPageFinishedHelper(), DATA_HTML, "text/html", false, baseUrl, |
| null); |
| return hasJavaScriptObject(jsObjectName, mActivityTestRule, mAwContents, mContentsClient); |
| } |
| |
| // The script is trying to set a global JavaScript object, so it is essentially the same |
| // with isJsObjectInjectedWhenLoadingUrl(). Having a wrapper method to make it clear for |
| // the context. |
| private boolean didScriptRunWhenLoading(final String baseUrl, final String objectName) |
| throws Throwable { |
| return isJsObjectInjectedWhenLoadingUrl(baseUrl, objectName); |
| } |
| |
| private String loadUrlFromPath(String path) throws Exception { |
| final String url = mTestServer.getURL(path); |
| mActivityTestRule.loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url); |
| return url; |
| } |
| |
| private static void assertUrlHasOrigin(final String url, final Uri origin) { |
| Uri uriFromServer = Uri.parse(url); |
| Assert.assertEquals(uriFromServer.getScheme(), origin.getScheme()); |
| Assert.assertEquals(uriFromServer.getHost(), origin.getHost()); |
| Assert.assertEquals(uriFromServer.getPort(), origin.getPort()); |
| } |
| |
| private static String createCrossOriginAccessTestPageHtml(final String frameUrl) { |
| return "<html>" |
| + "<body><div>I have an iframe</ div>" |
| + " <iframe src ='" + frameUrl + "'></iframe>" |
| + "</body></html>"; |
| } |
| |
| private static ScriptHandler addDocumentStartJavaScriptOnUiThread( |
| final AwContents awContents, final String script, final String[] allowedOriginRules) { |
| return TestThreadUtils.runOnUiThreadBlockingNoException( |
| () -> awContents.addDocumentStartJavaScript(script, allowedOriginRules)); |
| } |
| |
| private static void addWebMessageListenerOnUiThread(final AwContents awContents, |
| final String jsObjectName, final String[] allowedOriginRules, |
| final WebMessageListener listener) { |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> awContents.addWebMessageListener(jsObjectName, allowedOriginRules, listener)); |
| } |
| |
| private static void removeWebMessageListenerOnUiThread( |
| final AwContents awContents, final String jsObjectName) { |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> awContents.removeWebMessageListener(jsObjectName)); |
| } |
| |
| private static boolean hasJavaScriptObject(final String jsObjectName, |
| final AwActivityTestRule rule, final AwContents awContents, |
| final TestAwContentsClient contentsClient) throws Throwable { |
| final String result = rule.executeJavaScriptAndWaitForResult( |
| awContents, contentsClient, "typeof " + jsObjectName + " !== 'undefined'"); |
| return result.equals("true"); |
| } |
| |
| private static void addEventListener(final String func, final String funcName, |
| String jsObjectName, final AwActivityTestRule rule, final AwContents awContents, |
| final TestAwContentsClient contentsClient) throws Throwable { |
| String code = "let " + funcName + " = " + func + ";"; |
| code += jsObjectName + ".addEventListener('message', " + funcName + ");"; |
| rule.executeJavaScriptAndWaitForResult(awContents, contentsClient, code); |
| } |
| |
| private static void removeEventListener(final String funcName, final String jsObjectName, |
| final AwActivityTestRule rule, final AwContents awContents, |
| final TestAwContentsClient contentsClient) throws Throwable { |
| String code = jsObjectName + ".removeEventListener('message', " + funcName + ")"; |
| rule.executeJavaScriptAndWaitForResult(awContents, contentsClient, code); |
| } |
| |
| private static String parseOrigin(String url) { |
| final Uri uri = Uri.parse(url); |
| final StringBuilder sb = new StringBuilder(); |
| final String scheme = uri.getScheme(); |
| final int port = uri.getPort(); |
| |
| if (!scheme.isEmpty()) { |
| sb.append(scheme); |
| sb.append("://"); |
| } |
| sb.append(uri.getHost()); |
| if (port != -1) { |
| sb.append(":"); |
| sb.append(port); |
| } |
| return sb.toString(); |
| } |
| |
| private static Throwable getRootCauseException(Throwable exception) { |
| while (exception.getCause() != null) { |
| exception = exception.getCause(); |
| } |
| return exception; |
| } |
| } |