blob: ec62fdbb5568305aa70bdf983774d84d40545ec4 [file] [log] [blame]
// Copyright 2014 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.os.Handler;
import android.test.suitebuilder.annotation.SmallTest;
import android.webkit.JavascriptInterface;
import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout;
import static org.chromium.content.browser.test.util.TestCallbackHelperContainer.OnPageFinishedHelper;
import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.AwMessagePort;
import org.chromium.android_webview.AwMessagePortService;
import org.chromium.android_webview.test.util.CommonResources;
import org.chromium.base.ThreadUtils;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;
import org.chromium.net.test.util.TestWebServer;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
/**
* The tests for content postMessage API.
*/
@SuppressFBWarnings("DLS_DEAD_LOCAL_STORE")
public class PostMessageTest extends AwTestBase {
private static final String SOURCE_ORIGIN = "";
// Timeout to failure, in milliseconds
private static final long TIMEOUT = scaleTimeout(5000);
// Inject to the page to verify received messages.
private static class MessageObject {
private boolean mReady;
private String mData;
private String mOrigin;
private int[] mPorts;
private final Object mLock = new Object();
@JavascriptInterface
public void setMessageParams(String data, String origin, int[] ports) {
synchronized (mLock) {
mData = data;
mOrigin = origin;
mPorts = ports;
mReady = true;
mLock.notify();
}
}
public void waitForMessage() throws InterruptedException {
synchronized (mLock) {
long deadline = System.currentTimeMillis() + TIMEOUT;
while (!mReady && System.currentTimeMillis() < deadline) {
mLock.wait(deadline - System.currentTimeMillis());
}
}
}
public String getData() {
return mData;
}
public String getOrigin() {
return mOrigin;
}
public int[] getPorts() {
return mPorts;
}
}
private MessageObject mMessageObject;
private TestAwContentsClient mContentsClient;
private AwTestContainerView mTestContainerView;
private AwContents mAwContents;
private TestWebServer mWebServer;
@Override
protected void setUp() throws Exception {
super.setUp();
mMessageObject = new MessageObject();
mContentsClient = new TestAwContentsClient();
mTestContainerView = createAwTestContainerViewOnMainSync(mContentsClient);
mAwContents = mTestContainerView.getAwContents();
enableJavaScriptOnUiThread(mAwContents);
try {
runTestOnUiThread(new Runnable() {
@Override
public void run() {
mAwContents.addJavascriptInterface(mMessageObject, "messageObject");
}
});
} catch (Throwable t) {
throw new RuntimeException(t);
}
mWebServer = TestWebServer.start();
}
@Override
protected void tearDown() throws Exception {
mWebServer.shutdown();
super.tearDown();
}
private static final String WEBVIEW_MESSAGE = "from_webview";
private static final String JS_MESSAGE = "from_js";
private static final String TEST_PAGE =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " onmessage = function (e) {"
+ " messageObject.setMessageParams(e.data, e.origin, e.ports);"
+ " if (e.ports != null && e.ports.length > 0) {"
+ " e.ports[0].postMessage(\"" + JS_MESSAGE + "\");"
+ " }"
+ " }"
+ " </script>"
+ "</body></html>";
// Concats all the data fields of the received messages and makes it
// available as page title.
private static final String TITLE_FROM_POSTMESSAGE_TO_FRAME =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " var received = '';"
+ " onmessage = function (e) {"
+ " received += e.data;"
+ " document.title = received;"
+ " }"
+ " </script>"
+ "</body></html>";
// Concats all the data fields of the received messages to the transferred channel
// and makes it available as page title.
private static final String TITLE_FROM_POSTMESSAGE_TO_CHANNEL =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " var received = '';"
+ " onmessage = function (e) {"
+ " var myport = e.ports[0];"
+ " myport.onmessage = function (f) {"
+ " received += f.data;"
+ " document.title = received;"
+ " }"
+ " }"
+ " </script>"
+ "</body></html>";
// Call on non-UI thread.
private void expectTitle(final String title) throws Throwable {
CriteriaHelper.pollForUIThreadCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
updateFailureReason(
"Received title " + mAwContents.getTitle() + " while expecting " + title);
return mAwContents.getTitle().equals(title);
}
});
}
private void loadPage(String page) throws Throwable {
final String url = mWebServer.setResponse("/test.html", page,
CommonResources.getTextHtmlHeaders(true));
OnPageFinishedHelper onPageFinishedHelper = mContentsClient.getOnPageFinishedHelper();
int currentCallCount = onPageFinishedHelper.getCallCount();
loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url);
onPageFinishedHelper.waitForCallback(currentCallCount);
}
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostMessageToMainFrame() throws Throwable {
loadPage(TEST_PAGE);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
mAwContents.postMessageToFrame(null, WEBVIEW_MESSAGE, mWebServer.getBaseUrl(),
null);
}
});
mMessageObject.waitForMessage();
assertEquals(WEBVIEW_MESSAGE, mMessageObject.getData());
assertEquals(SOURCE_ORIGIN, mMessageObject.getOrigin());
}
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testTransferringSamePortTwiceViaPostMessageToFrameNotAllowed() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
mAwContents.postMessageToFrame(null, "1", mWebServer.getBaseUrl(),
new AwMessagePort[]{channel[1]});
// Retransfer the port. This should fail with an exception.
try {
mAwContents.postMessageToFrame(null, "2", mWebServer.getBaseUrl(),
new AwMessagePort[]{channel[1]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
fail();
}
});
boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
}
// There are two cases that put a port in a started state.
// 1. posting a message
// 2. setting an event handler.
// A started port cannot return to "non-started" state. The four tests below verifies
// these conditions for both conditions, using message ports and message channels.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testStartedPortCannotBeTransferredUsingPostMessageToFrame1() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
channel[1].postMessage("1", null);
try {
mAwContents.postMessageToFrame(null, "2", mWebServer.getBaseUrl(),
new AwMessagePort[]{channel[1]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
fail();
}
});
boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
}
// see documentation in testStartedPortCannotBeTransferredUsingPostMessageToFrame1
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testStartedPortCannotBeTransferredUsingPostMessageToFrame2() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
// set a web event handler, this puts the port in a started state.
channel[1].setMessageCallback(new AwMessagePort.MessageCallback() {
@Override
public void onMessage(String message, AwMessagePort[] sentPorts) { }
}, null);
try {
mAwContents.postMessageToFrame(null, "2", mWebServer.getBaseUrl(),
new AwMessagePort[]{channel[1]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
fail();
}
});
boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
}
// see documentation in testStartedPortCannotBeTransferredUsingPostMessageToFrame1
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testStartedPortCannotBeTransferredUsingMessageChannel1() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel1 = mAwContents.createMessageChannel();
channel1[1].postMessage("1", null);
AwMessagePort[] channel2 = mAwContents.createMessageChannel();
try {
channel2[0].postMessage("2", new AwMessagePort[]{channel1[1]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
fail();
}
});
boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
}
// see documentation in testStartedPortCannotBeTransferredUsingPostMessageToFrame1
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testStartedPortCannotBeTransferredUsingMessageChannel2() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel1 = mAwContents.createMessageChannel();
// set a web event handler, this puts the port in a started state.
channel1[1].setMessageCallback(new AwMessagePort.MessageCallback() {
@Override
public void onMessage(String message, AwMessagePort[] sentPorts) { }
}, null);
AwMessagePort[] channel2 = mAwContents.createMessageChannel();
try {
channel2[0].postMessage("1", new AwMessagePort[]{channel1[1]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
fail();
}
});
boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
}
// channel[0] and channel[1] are entangled ports, establishing a channel. Verify
// it is not allowed to transfer channel[0] on channel[0].postMessage.
// TODO(sgurun) Note that the related case of posting channel[1] via
// channel[0].postMessage does not throw a JS exception at present. We do not throw
// an exception in this case either since the information of entangled port is not
// available at the source port. We need a new mechanism to implement to prevent
// this case.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testTransferringSourcePortViaMessageChannelNotAllowed() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
try {
channel[0].postMessage("1", new AwMessagePort[]{channel[0]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
fail();
}
});
boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
}
// Verify a closed port cannot be transferred to a frame.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testSendClosedPortToFrameNotAllowed() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
channel[1].close();
try {
mAwContents.postMessageToFrame(null, "1", mWebServer.getBaseUrl(),
new AwMessagePort[]{channel[1]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
fail();
}
});
boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
}
// Verify a closed port cannot be transferred to a port.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testSendClosedPortToPortNotAllowed() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel1 = mAwContents.createMessageChannel();
AwMessagePort[] channel2 = mAwContents.createMessageChannel();
channel2[1].close();
try {
channel1[0].postMessage("1", new AwMessagePort[]{channel2[1]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
fail();
}
});
boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
}
// Verify messages cannot be posted to closed ports.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostMessageToClosedPortNotAllowed() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
channel[0].close();
try {
channel[0].postMessage("1", null);
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
fail();
}
});
boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
}
// Verify messages posted before closing a port is received at the destination port.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testMessagesPostedBeforeClosingPortAreTransferred() throws Throwable {
loadPage(TITLE_FROM_POSTMESSAGE_TO_CHANNEL);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
mAwContents.postMessageToFrame(null, "1", mWebServer.getBaseUrl(),
new AwMessagePort[]{channel[1]});
channel[0].postMessage("2", null);
channel[0].postMessage("3", null);
channel[0].close();
}
});
expectTitle("23");
}
// Verify a transferred port using postmessagetoframe cannot be closed.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testClosingTransferredPortToFrameThrowsAnException() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
mAwContents.postMessageToFrame(null, "1", mWebServer.getBaseUrl(),
new AwMessagePort[]{channel[1]});
try {
channel[1].close();
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
fail();
}
});
boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
}
// Verify a transferred port using postmessagetoframe cannot be closed.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testClosingTransferredPortToChannelThrowsAnException() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel1 = mAwContents.createMessageChannel();
mAwContents.postMessageToFrame(null, "1", mWebServer.getBaseUrl(),
new AwMessagePort[]{channel1[1]});
AwMessagePort[] channel2 = mAwContents.createMessageChannel();
channel1[0].postMessage("2", new AwMessagePort[]{channel2[0]});
try {
channel2[0].close();
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
fail();
}
});
boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
}
// Create two message channels, and while they are in pending state, transfer the
// second one in the first one.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPendingPortCanBeTransferredInPendingPort() throws Throwable {
loadPage(TITLE_FROM_POSTMESSAGE_TO_CHANNEL);
final TestMessagePort testPort =
new TestMessagePort(getAwBrowserContext().getMessagePortService());
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel1 = mAwContents.createMessageChannel();
mAwContents.postMessageToFrame(null, "1", mWebServer.getBaseUrl(),
new AwMessagePort[]{channel1[1]});
AwMessagePort[] channel2 = mAwContents.createMessageChannel();
channel1[0].postMessage("2", new AwMessagePort[]{channel2[0]});
}
});
expectTitle("2");
}
private static class ChannelContainer {
private boolean mReady;
private AwMessagePort[] mChannel;
private final Object mLock = new Object();
private String mMessage = "";
private int mCount;
private int mWaitCount;
public ChannelContainer() {
this(1);
}
public ChannelContainer(int n) {
mWaitCount = n;
}
public void set(AwMessagePort[] channel) {
mChannel = channel;
}
public AwMessagePort[] get() {
return mChannel;
}
public void setMessage(String message) {
synchronized (mLock) {
mMessage += message;
if (++mCount < mWaitCount) return;
mReady = true;
mLock.notify();
}
}
public int getMessageCount() {
return mCount;
}
public String getMessage() {
return mMessage;
}
public void waitForMessage() throws InterruptedException {
synchronized (mLock) {
long deadline = System.currentTimeMillis() + TIMEOUT;
while (!mReady && System.currentTimeMillis() < deadline) {
mLock.wait(deadline - System.currentTimeMillis());
}
}
}
}
// Verify that messages from JS can be waited on a UI thread.
// TODO(sgurun) this test turned out to be flaky. When it fails, it always fails in IPC.
// When a postmessage is received, an IPC message is sent from browser to renderer
// to convert the postmessage from WebSerializedScriptValue to a string. The IPC is sent
// and seems to be received by IPC in renderer, but then nothing else seems to happen.
// The issue seems like blocking the UI thread causes a racing SYNC ipc from renderer
// to browser to block waiting for UI thread, and this would in turn block renderer
// doing the conversion.
@DisabledTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testReceiveMessageInBackgroundThread() throws Throwable {
loadPage(TEST_PAGE);
final ChannelContainer channelContainer = new ChannelContainer();
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
// verify communication from JS to Java.
channelContainer.set(channel);
channel[0].setMessageCallback(new AwMessagePort.MessageCallback() {
@Override
public void onMessage(String message, AwMessagePort[] sentPorts) {
channelContainer.setMessage(message);
}
}, null);
mAwContents.postMessageToFrame(null, WEBVIEW_MESSAGE, mWebServer.getBaseUrl(),
new AwMessagePort[]{channel[1]});
}
});
mMessageObject.waitForMessage();
assertEquals(WEBVIEW_MESSAGE, mMessageObject.getData());
assertEquals(SOURCE_ORIGIN, mMessageObject.getOrigin());
// verify that one message port is received at the js side
assertEquals(1, mMessageObject.getPorts().length);
// wait until we receive a message from JS
runTestOnUiThread(new Runnable() {
@Override
public void run() {
try {
channelContainer.waitForMessage();
} catch (InterruptedException e) {
// ignore.
}
}
});
assertEquals(JS_MESSAGE, channelContainer.getMessage());
}
private static final String ECHO_PAGE =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " onmessage = function (e) {"
+ " var myPort = e.ports[0];"
+ " myPort.onmessage = function(e) {"
+ " myPort.postMessage(e.data + \"" + JS_MESSAGE + "\"); }"
+ " }"
+ " </script>"
+ "</body></html>";
// Call on non-UI thread.
private void waitUntilPortReady(final AwMessagePort port) throws Throwable {
CriteriaHelper.pollForUIThreadCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return port.isReady();
}
});
}
private static final String HELLO = "HELLO";
// Message channels are created on UI thread in a pending state. They are
// initialized at a later stage. Verify that a message port that is initialized
// can be transferred to JS and full communication can happen on it.
// Do this by sending a message to JS and let it echo'ing the message with
// some text prepended to it.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testMessageChannelUsingInitializedPort() throws Throwable {
final ChannelContainer channelContainer = new ChannelContainer();
loadPage(ECHO_PAGE);
final AwMessagePort[] channel = ThreadUtils.runOnUiThreadBlocking(
new Callable<AwMessagePort[]>() {
@Override
public AwMessagePort[] call() {
return mAwContents.createMessageChannel();
}
});
waitUntilPortReady(channel[0]);
waitUntilPortReady(channel[1]);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
channel[0].setMessageCallback(new AwMessagePort.MessageCallback() {
@Override
public void onMessage(String message, AwMessagePort[] sentPorts) {
channelContainer.setMessage(message);
}
}, null);
mAwContents.postMessageToFrame(null, WEBVIEW_MESSAGE, mWebServer.getBaseUrl(),
new AwMessagePort[]{channel[1]});
channel[0].postMessage(HELLO, null);
}
});
// wait for the asynchronous response from JS
channelContainer.waitForMessage();
assertEquals(HELLO + JS_MESSAGE, channelContainer.getMessage());
}
// Verify that a message port can be used immediately (even if it is in
// pending state) after creation. In particular make sure the message port can be
// transferred to JS and full communication can happen on it.
// Do this by sending a message to JS and let it echo'ing the message with
// some text prepended to it.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testMessageChannelUsingPendingPort() throws Throwable {
final ChannelContainer channelContainer = new ChannelContainer();
loadPage(ECHO_PAGE);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
channel[0].setMessageCallback(new AwMessagePort.MessageCallback() {
@Override
public void onMessage(String message, AwMessagePort[] sentPorts) {
channelContainer.setMessage(message);
}
}, null);
mAwContents.postMessageToFrame(null, WEBVIEW_MESSAGE, mWebServer.getBaseUrl(),
new AwMessagePort[]{channel[1]});
channel[0].postMessage(HELLO, null);
}
});
// Wait for the asynchronous response from JS.
channelContainer.waitForMessage();
assertEquals(HELLO + JS_MESSAGE, channelContainer.getMessage());
}
// Verify that a message port can be used for message transfer when both
// ports are owned by same Webview.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testMessageChannelCommunicationWithinWebView() throws Throwable {
final ChannelContainer channelContainer = new ChannelContainer();
loadPage(ECHO_PAGE);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
channel[1].setMessageCallback(new AwMessagePort.MessageCallback() {
@Override
public void onMessage(String message, AwMessagePort[] sentPorts) {
channelContainer.setMessage(message);
}
}, null);
channel[0].postMessage(HELLO, null);
}
});
// Wait for the asynchronous response from JS.
channelContainer.waitForMessage();
assertEquals(HELLO, channelContainer.getMessage());
}
// Post a message with a pending port to a frame and then post a bunch of messages
// after that. Make sure that they are not ordered at the receiver side.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostMessageToFrameNotReordersMessages() throws Throwable {
loadPage(TITLE_FROM_POSTMESSAGE_TO_FRAME);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
mAwContents.postMessageToFrame(null, "1", mWebServer.getBaseUrl(),
new AwMessagePort[]{channel[1]});
mAwContents.postMessageToFrame(null, "2", mWebServer.getBaseUrl(), null);
mAwContents.postMessageToFrame(null, "3", mWebServer.getBaseUrl(), null);
}
});
expectTitle("123");
}
private static final String RECEIVE_JS_MESSAGE_CHANNEL_PAGE =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " var received ='';"
+ " var mc = new MessageChannel();"
+ " mc.port1.onmessage = function (e) {"
+ " received += e.data;"
+ " document.title = received;"
+ " if (e.data == '2') { mc.port1.postMessage('3'); }"
+ " };"
+ " onmessage = function (e) {"
+ " var myPort = e.ports[0];"
+ " myPort.postMessage('from window', [mc.port2]);"
+ " }"
+ " </script>"
+ "</body></html>";
// Test webview can use a message port received from JS for full duplex communication.
// Test steps:
// 1. Java creates a message channel, and send one port to JS
// 2. JS creates a new message channel and sends one port to Java using the channel in 1
// 3. Java sends a message using the new channel in 2.
// 4. Js responds to this message using the channel in 2.
// 5. Java responds to message in 4 using the channel in 2.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testCanUseReceivedAwMessagePortFromJS() throws Throwable {
loadPage(RECEIVE_JS_MESSAGE_CHANNEL_PAGE);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
mAwContents.postMessageToFrame(null, "1", mWebServer.getBaseUrl(),
new AwMessagePort[]{channel[1]});
channel[0].setMessageCallback(new AwMessagePort.MessageCallback() {
@Override
public void onMessage(String message, final AwMessagePort[] p) {
p[0].setMessageCallback(new AwMessagePort.MessageCallback() {
@Override
public void onMessage(String message, AwMessagePort[] q) {
assertEquals("3", message);
p[0].postMessage("4", null);
}
}, null);
p[0].postMessage("2", null);
}
}, null);
}
});
expectTitle("24");
}
private static class TestMessagePort extends AwMessagePort {
private boolean mReady;
private AwMessagePort mPort;
private final Object mLock = new Object();
public TestMessagePort(AwMessagePortService service) {
super(service);
}
public void setMessagePort(AwMessagePort port) {
mPort = port;
}
public void setReady(boolean ready) {
synchronized (mLock) {
mReady = ready;
}
}
@Override
public boolean isReady() {
synchronized (mLock) {
return mReady;
}
}
@Override
public int portId() {
return mPort.portId();
}
@Override
public void setPortId(int id) {
mPort.setPortId(id);
}
@Override
public void close() {
mPort.close();
}
@Override
public boolean isClosed() {
return mPort.isClosed();
}
@Override
public void setMessageCallback(MessageCallback messageCallback, Handler handler) {
mPort.setMessageCallback(messageCallback, handler);
}
@Override
public void onMessage(String message, AwMessagePort[] sentPorts) {
mPort.onMessage(message, sentPorts);
}
@Override
public void postMessage(String message, AwMessagePort[] sentPorts) throws
IllegalStateException {
mPort.postMessage(message, sentPorts);
}
}
// Post a message with a pending port to a frame and then post a message that
// is pending after that. Make sure that when first message becomes ready,
// the subsequent not-ready message is not sent.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostMessageToFrameNotSendsPendingMessages() throws Throwable {
loadPage(TITLE_FROM_POSTMESSAGE_TO_FRAME);
final TestMessagePort testPort =
new TestMessagePort(getAwBrowserContext().getMessagePortService());
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
mAwContents.postMessageToFrame(null, "1", mWebServer.getBaseUrl(),
new AwMessagePort[]{channel[1]});
mAwContents.postMessageToFrame(null, "2", mWebServer.getBaseUrl(), null);
AwMessagePort[] channel2 = mAwContents.createMessageChannel();
// Test port is in a pending state so it should not be transferred.
testPort.setMessagePort(channel2[0]);
mAwContents.postMessageToFrame(null, "3", mWebServer.getBaseUrl(),
new AwMessagePort[]{testPort});
}
});
expectTitle("12");
}
private static final String WORKER_MESSAGE = "from_worker";
// Listen for messages. Pass port 1 to worker and use port 2 to receive messages from
// from worker.
private static final String TEST_PAGE_FOR_PORT_TRANSFER =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " var worker = new Worker(\"worker.js\");"
+ " onmessage = function (e) {"
+ " if (e.data == \"" + WEBVIEW_MESSAGE + "\") {"
+ " worker.postMessage(\"worker_port\", [e.ports[0]]);"
+ " var messageChannelPort = e.ports[1];"
+ " messageChannelPort.onmessage = receiveWorkerMessage;"
+ " }"
+ " };"
+ " function receiveWorkerMessage(e) {"
+ " if (e.data == \"" + WORKER_MESSAGE + "\") {"
+ " messageObject.setMessageParams(e.data, e.origin, e.ports);"
+ " }"
+ " };"
+ " </script>"
+ "</body></html>";
private static final String WORKER_SCRIPT =
"onmessage = function(e) {"
+ " if (e.data == \"worker_port\") {"
+ " var toWindow = e.ports[0];"
+ " toWindow.postMessage(\"" + WORKER_MESSAGE + "\");"
+ " toWindow.start();"
+ " }"
+ "}";
// Test if message ports created at the native side can be transferred
// to JS side, to establish a communication channel between a worker and a frame.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testTransferPortsToWorker() throws Throwable {
mWebServer.setResponse("/worker.js", WORKER_SCRIPT,
CommonResources.getTextJavascriptHeaders(true));
loadPage(TEST_PAGE_FOR_PORT_TRANSFER);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
mAwContents.postMessageToFrame(null, WEBVIEW_MESSAGE, mWebServer.getBaseUrl(),
new AwMessagePort[]{channel[0], channel[1]});
}
});
mMessageObject.waitForMessage();
assertEquals(WORKER_MESSAGE, mMessageObject.getData());
}
private static final String POPUP_MESSAGE = "from_popup";
private static final String POPUP_URL = "/popup.html";
private static final String IFRAME_URL = "/iframe.html";
private static final String MAIN_PAGE_FOR_POPUP_TEST = "<!DOCTYPE html><html>"
+ "<head>"
+ " <script>"
+ " function createPopup() {"
+ " var popupWindow = window.open('" + POPUP_URL + "');"
+ " onmessage = function(e) {"
+ " popupWindow.postMessage(e.data, '*', e.ports);"
+ " };"
+ " }"
+ " </script>"
+ "</head>"
+ "</html>";
// Sends message and ports to the iframe.
private static final String POPUP_PAGE_WITH_IFRAME = "<!DOCTYPE html><html>"
+ "<script>"
+ " onmessage = function(e) {"
+ " var iframe = document.getElementsByTagName('iframe')[0];"
+ " iframe.contentWindow.postMessage('" + POPUP_MESSAGE + "', '*', e.ports);"
+ " };"
+ "</script>"
+ "<body><iframe src='" + IFRAME_URL + "'></iframe></body>"
+ "</html>";
// Test if WebView can post a message from/to a popup window owning a message port.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostMessageToPopup() throws Throwable {
triggerPopup(mAwContents, mContentsClient, mWebServer, MAIN_PAGE_FOR_POPUP_TEST, ECHO_PAGE,
POPUP_URL, "createPopup()");
connectPendingPopup(mAwContents);
final ChannelContainer channelContainer = new ChannelContainer();
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
channelContainer.set(channel);
channel[0].setMessageCallback(new AwMessagePort.MessageCallback() {
@Override
public void onMessage(String message, AwMessagePort[] sentPorts) {
channelContainer.setMessage(message);
}
}, null);
mAwContents.postMessageToFrame(null, WEBVIEW_MESSAGE, mWebServer.getBaseUrl(),
new AwMessagePort[] {channel[1]});
channel[0].postMessage(HELLO, null);
}
});
channelContainer.waitForMessage();
assertEquals(HELLO + JS_MESSAGE, channelContainer.getMessage());
}
// Test if WebView can post a message from/to an iframe in a popup window.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostMessageToIframeInsidePopup() throws Throwable {
mWebServer.setResponse(IFRAME_URL, ECHO_PAGE, null);
triggerPopup(mAwContents, mContentsClient, mWebServer, MAIN_PAGE_FOR_POPUP_TEST,
POPUP_PAGE_WITH_IFRAME, POPUP_URL, "createPopup()");
connectPendingPopup(mAwContents);
final ChannelContainer channelContainer = new ChannelContainer();
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
channelContainer.set(channel);
channel[0].setMessageCallback(new AwMessagePort.MessageCallback() {
@Override
public void onMessage(String message, AwMessagePort[] sentPorts) {
channelContainer.setMessage(message);
}
}, null);
mAwContents.postMessageToFrame(null, WEBVIEW_MESSAGE, mWebServer.getBaseUrl(),
new AwMessagePort[] {channel[1]});
channel[0].postMessage(HELLO, null);
}
});
channelContainer.waitForMessage();
assertEquals(HELLO + JS_MESSAGE, channelContainer.getMessage());
}
private static final String TEST_PAGE_FOR_UNSUPPORTED_MESSAGES = "<!DOCTYPE html><html><body>"
+ " <script>"
+ " onmessage = function (e) {"
+ " e.ports[0].postMessage(null);"
+ " e.ports[0].postMessage(undefined);"
+ " e.ports[0].postMessage(NaN);"
+ " e.ports[0].postMessage(0);"
+ " e.ports[0].postMessage(new Set());"
+ " e.ports[0].postMessage({});"
+ " e.ports[0].postMessage(['1','2','3']);"
+ " e.ports[0].postMessage('" + JS_MESSAGE + "');"
+ " }"
+ " </script>"
+ "</body></html>";
// Make sure that postmessage can handle unsupported messages gracefully.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostUnsupportedWebMessageToApp() throws Throwable {
loadPage(TEST_PAGE_FOR_UNSUPPORTED_MESSAGES);
final ChannelContainer channelContainer = new ChannelContainer();
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
channelContainer.set(channel);
channel[0].setMessageCallback(new AwMessagePort.MessageCallback() {
@Override
public void onMessage(String message, AwMessagePort[] sentPorts) {
channelContainer.setMessage(message);
}
}, null);
mAwContents.postMessageToFrame(null, WEBVIEW_MESSAGE, mWebServer.getBaseUrl(),
new AwMessagePort[] {channel[1]});
}
});
channelContainer.waitForMessage();
assertEquals(JS_MESSAGE, channelContainer.getMessage());
// Assert that onMessage is called only once.
assertEquals(1, channelContainer.getMessageCount());
}
private static final String TEST_TRANSFER_EMPTY_PORTS = "<!DOCTYPE html><html><body>"
+ " <script>"
+ " onmessage = function (e) {"
+ " e.ports[0].postMessage('1', null);"
+ " e.ports[0].postMessage('2', []);"
+ " }"
+ " </script>"
+ "</body></html>";
// Make sure that postmessage can handle unsupported messages gracefully.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testTransferEmptyPortsArray() throws Throwable {
loadPage(TEST_TRANSFER_EMPTY_PORTS);
final ChannelContainer channelContainer = new ChannelContainer(2);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
AwMessagePort[] channel = mAwContents.createMessageChannel();
channelContainer.set(channel);
channel[0].setMessageCallback(new AwMessagePort.MessageCallback() {
@Override
public void onMessage(String message, AwMessagePort[] sentPorts) {
channelContainer.setMessage(message);
}
}, null);
mAwContents.postMessageToFrame(null, WEBVIEW_MESSAGE, mWebServer.getBaseUrl(),
new AwMessagePort[] {channel[1]});
}
});
channelContainer.waitForMessage();
assertEquals("12", channelContainer.getMessage());
}
}