| // Copyright 2021 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.chrome.browser.customtabs; |
| |
| import static org.hamcrest.Matchers.is; |
| import static org.junit.Assert.assertEquals; |
| |
| import static org.chromium.base.test.util.Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE; |
| import static org.chromium.chrome.browser.customtabs.CustomTabActivityTestRule.LONG_TIMEOUT_MS; |
| |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.support.test.InstrumentationRegistry; |
| |
| import androidx.browser.customtabs.CustomTabsCallback; |
| import androidx.browser.customtabs.CustomTabsIntent; |
| import androidx.browser.customtabs.CustomTabsService; |
| import androidx.browser.customtabs.CustomTabsSession; |
| import androidx.browser.customtabs.CustomTabsSessionToken; |
| import androidx.test.filters.SmallTest; |
| |
| import org.hamcrest.Matchers; |
| import org.junit.After; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import org.chromium.base.library_loader.LibraryLoader; |
| import org.chromium.base.task.PostTask; |
| import org.chromium.base.test.util.CallbackHelper; |
| import org.chromium.base.test.util.CommandLineFlags; |
| import org.chromium.base.test.util.Criteria; |
| import org.chromium.base.test.util.CriteriaHelper; |
| import org.chromium.base.test.util.DisabledTest; |
| import org.chromium.base.test.util.Restriction; |
| import org.chromium.chrome.browser.document.ChromeLauncherActivity; |
| import org.chromium.chrome.browser.flags.ChromeSwitches; |
| import org.chromium.chrome.browser.tab.Tab; |
| import org.chromium.chrome.test.ChromeJUnit4ClassRunner; |
| import org.chromium.chrome.test.util.ChromeTabUtils; |
| import org.chromium.content_public.browser.LoadUrlParams; |
| import org.chromium.content_public.browser.UiThreadTaskTraits; |
| import org.chromium.content_public.browser.WebContentsObserver; |
| import org.chromium.content_public.browser.test.util.TestThreadUtils; |
| import org.chromium.content_public.browser.test.util.WebContentsUtils; |
| import org.chromium.net.test.util.TestWebServer; |
| |
| /** |
| * Integration tests for the Custom Tab post message support. |
| */ |
| @RunWith(ChromeJUnit4ClassRunner.class) |
| @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE}) |
| public class CustomTabPostMessageTest { |
| private static final int BEFORE_MAY_LAUNCH_URL = 0; |
| private static final int BEFORE_INTENT = 1; |
| private static final int AFTER_INTENT = 2; |
| |
| private static final String JS_MESSAGE = "from_js"; |
| private static final String TEST_PAGE = "/chrome/test/data/android/google.html"; |
| private static final String TEST_PAGE_2 = "/chrome/test/data/android/test.html"; |
| |
| 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>"; |
| private static final String MESSAGE_FROM_PAGE_TO_CHANNEL = "<!DOCTYPE html><html><body>" |
| + " <script>" |
| + " onmessage = function (e) {" |
| + " if (e.ports != null && e.ports.length > 0) {" |
| + " e.ports[0].postMessage(\"" + JS_MESSAGE + "\");" |
| + " }" |
| + " }" |
| + " </script>" |
| + "</body></html>"; |
| |
| @Rule |
| public CustomTabActivityTestRule mCustomTabActivityTestRule = new CustomTabActivityTestRule(); |
| |
| private String mTestPage; |
| private String mTestPage2; |
| private CustomTabsConnection mConnectionToCleanup; |
| private TestWebServer mWebServer; |
| |
| @Before |
| public void setUp() throws Exception { |
| mTestPage = mCustomTabActivityTestRule.getTestServer().getURL(TEST_PAGE); |
| mTestPage2 = mCustomTabActivityTestRule.getTestServer().getURL(TEST_PAGE_2); |
| LibraryLoader.getInstance().ensureInitialized(); |
| mWebServer = TestWebServer.start(); |
| } |
| |
| @After |
| public void tearDown() { |
| if (mConnectionToCleanup != null) CustomTabsTestUtils.cleanupSessions(mConnectionToCleanup); |
| if (mWebServer != null) mWebServer.shutdown(); |
| } |
| |
| private void waitForTitle(String newTitle) { |
| Tab currentTab = mCustomTabActivityTestRule.getActivity().getActivityTab(); |
| ChromeTabUtils.waitForTitle(currentTab, newTitle); |
| } |
| |
| private void setCanUseHiddenTabForSession( |
| CustomTabsConnection connection, CustomTabsSessionToken token, boolean useHiddenTab) { |
| assert mConnectionToCleanup == null || mConnectionToCleanup == connection; |
| // Save the connection. In case the hidden tab is not consumed by the test, ensure that it |
| // is properly cleaned up after the test. |
| mConnectionToCleanup = connection; |
| connection.setCanUseHiddenTabForSession(token, useHiddenTab); |
| } |
| |
| private static void ensureCompletedSpeculationForUrl( |
| final CustomTabsConnection connection, final String url) { |
| CriteriaHelper.pollUiThread(() -> { |
| Criteria.checkThat("Tab was not created", connection.getSpeculationParamsForTesting(), |
| Matchers.notNullValue()); |
| }, LONG_TIMEOUT_MS, CriteriaHelper.DEFAULT_POLLING_INTERVAL); |
| ChromeTabUtils.waitForTabPageLoaded(connection.getSpeculationParamsForTesting().tab, url); |
| } |
| |
| /** |
| * Tests that basic postMessage functionality works through sending a single postMessage |
| * request. |
| */ |
| @Test |
| @SmallTest |
| public void testPostMessageBasic() throws Exception { |
| final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait(); |
| Context context = InstrumentationRegistry.getTargetContext(); |
| Intent intent = CustomTabsTestUtils.createMinimalCustomTabIntent(context, mTestPage); |
| final CustomTabsSessionToken token = |
| CustomTabsSessionToken.getSessionTokenFromIntent(intent); |
| Assert.assertTrue(connection.newSession(token)); |
| Assert.assertTrue(connection.requestPostMessageChannel(token, null)); |
| mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent); |
| CriteriaHelper.pollInstrumentationThread(() -> { |
| final Tab currentTab = mCustomTabActivityTestRule.getActivity().getActivityTab(); |
| Criteria.checkThat(ChromeTabUtils.getUrlStringOnUiThread(currentTab), is(mTestPage)); |
| }); |
| Assert.assertTrue( |
| connection.postMessage(token, "Message", null) == CustomTabsService.RESULT_SUCCESS); |
| TestThreadUtils.runOnUiThreadBlocking( |
| (Runnable) () |
| -> mCustomTabActivityTestRule.getActivity().getActivityTab().loadUrl( |
| new LoadUrlParams(mTestPage2))); |
| CriteriaHelper.pollUiThread(() -> { |
| final Tab currentTab = mCustomTabActivityTestRule.getActivity().getActivityTab(); |
| return ChromeTabUtils.isLoadingAndRenderingDone(currentTab); |
| }); |
| Assert.assertTrue(connection.postMessage(token, "Message", null) |
| == CustomTabsService.RESULT_FAILURE_MESSAGING_ERROR); |
| } |
| |
| /** |
| * Tests that postMessage channel is not functioning after web contents get destroyed and also |
| * not breaking things. |
| */ |
| @Test |
| @SmallTest |
| public void testPostMessageWebContentsDestroyed() throws Exception { |
| final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait(); |
| Context context = InstrumentationRegistry.getTargetContext(); |
| Intent intent = CustomTabsTestUtils.createMinimalCustomTabIntent(context, mTestPage); |
| final CustomTabsSessionToken token = |
| CustomTabsSessionToken.getSessionTokenFromIntent(intent); |
| Assert.assertTrue(connection.newSession(token)); |
| Assert.assertTrue(connection.requestPostMessageChannel(token, null)); |
| mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent); |
| CriteriaHelper.pollInstrumentationThread(() -> { |
| final Tab currentTab = mCustomTabActivityTestRule.getActivity().getActivityTab(); |
| Criteria.checkThat(ChromeTabUtils.getUrlStringOnUiThread(currentTab), is(mTestPage)); |
| }); |
| Assert.assertTrue( |
| connection.postMessage(token, "Message", null) == CustomTabsService.RESULT_SUCCESS); |
| |
| final CallbackHelper renderProcessCallback = new CallbackHelper(); |
| TestThreadUtils.runOnUiThreadBlocking(() -> { |
| new WebContentsObserver(mCustomTabActivityTestRule.getWebContents()) { |
| @Override |
| public void renderProcessGone(boolean wasOomProtected) { |
| renderProcessCallback.notifyCalled(); |
| } |
| }; |
| }); |
| PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> { |
| WebContentsUtils.simulateRendererKilled( |
| mCustomTabActivityTestRule.getActivity().getActivityTab().getWebContents(), |
| false); |
| }); |
| renderProcessCallback.waitForCallback(0); |
| Assert.assertTrue(connection.postMessage(token, "Message", null) |
| == CustomTabsService.RESULT_FAILURE_MESSAGING_ERROR); |
| } |
| |
| /** |
| * Tests whether validatePostMessageOrigin is necessary for making successful postMessage |
| * requests. |
| */ |
| @Test |
| @SmallTest |
| public void testPostMessageRequiresValidation() throws Exception { |
| final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait(); |
| Context context = InstrumentationRegistry.getTargetContext(); |
| Intent intent = CustomTabsTestUtils.createMinimalCustomTabIntent(context, mTestPage); |
| final CustomTabsSessionToken token = |
| CustomTabsSessionToken.getSessionTokenFromIntent(intent); |
| Assert.assertTrue(connection.newSession(token)); |
| mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent); |
| CriteriaHelper.pollInstrumentationThread(() -> { |
| final Tab currentTab = mCustomTabActivityTestRule.getActivity().getActivityTab(); |
| Criteria.checkThat(ChromeTabUtils.getUrlStringOnUiThread(currentTab), is(mTestPage)); |
| }); |
| Assert.assertTrue(connection.postMessage(token, "Message", null) |
| == CustomTabsService.RESULT_FAILURE_MESSAGING_ERROR); |
| } |
| |
| /** |
| * Tests the sent postMessage requests not only return success, but is also received by page. |
| */ |
| @Test |
| @SmallTest |
| public void testPostMessageReceivedInPage() throws Exception { |
| final String url = |
| mWebServer.setResponse("/test.html", TITLE_FROM_POSTMESSAGE_TO_CHANNEL, null); |
| final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait(); |
| Context context = InstrumentationRegistry.getTargetContext(); |
| Intent intent = CustomTabsTestUtils.createMinimalCustomTabIntent(context, url); |
| final CustomTabsSessionToken token = |
| CustomTabsSessionToken.getSessionTokenFromIntent(intent); |
| Assert.assertTrue(connection.newSession(token)); |
| Assert.assertTrue(connection.requestPostMessageChannel(token, null)); |
| mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent); |
| CriteriaHelper.pollInstrumentationThread(() -> { |
| final Tab currentTab = mCustomTabActivityTestRule.getActivity().getActivityTab(); |
| Criteria.checkThat(ChromeTabUtils.getUrlStringOnUiThread(currentTab), is(url)); |
| }); |
| Assert.assertTrue(connection.postMessage(token, "New title", null) |
| == CustomTabsService.RESULT_SUCCESS); |
| waitForTitle("New title"); |
| } |
| |
| /** |
| * Tests the postMessage requests sent from the page is received on the client side. |
| */ |
| @Test |
| @SmallTest |
| public void testPostMessageReceivedFromPage() throws Exception { |
| final CallbackHelper messageChannelHelper = new CallbackHelper(); |
| final CallbackHelper onPostMessageHelper = new CallbackHelper(); |
| final String url = mWebServer.setResponse("/test.html", MESSAGE_FROM_PAGE_TO_CHANNEL, null); |
| CustomTabsTestUtils.warmUpAndWait(); |
| final CustomTabsSession session = |
| CustomTabsTestUtils |
| .bindWithCallback(new CustomTabsCallback() { |
| @Override |
| public void onMessageChannelReady(Bundle extras) { |
| messageChannelHelper.notifyCalled(); |
| } |
| |
| @Override |
| public void onPostMessage(String message, Bundle extras) { |
| onPostMessageHelper.notifyCalled(); |
| } |
| }) |
| .session; |
| session.requestPostMessageChannel(Uri.parse("https://www.example.com/")); |
| Intent intent = new CustomTabsIntent.Builder(session).build().intent; |
| intent.setData(Uri.parse(url)); |
| intent.setComponent(new ComponentName( |
| InstrumentationRegistry.getTargetContext(), ChromeLauncherActivity.class)); |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| |
| Assert.assertTrue(session.postMessage("Message", null) |
| == CustomTabsService.RESULT_FAILURE_MESSAGING_ERROR); |
| |
| mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent); |
| messageChannelHelper.waitForCallback(0); |
| onPostMessageHelper.waitForCallback(0); |
| } |
| |
| /** |
| * Tests the postMessage requests sent from the page is received on the client side even though |
| * the request is sent after the page is created. |
| */ |
| @Test |
| @SmallTest |
| @DisabledTest(message = "https://crbug.com/692025") |
| public void testPostMessageReceivedFromPageWithLateRequest() throws Exception { |
| final CallbackHelper messageChannelHelper = new CallbackHelper(); |
| final CallbackHelper onPostMessageHelper = new CallbackHelper(); |
| final String url = mWebServer.setResponse("/test.html", MESSAGE_FROM_PAGE_TO_CHANNEL, null); |
| CustomTabsTestUtils.warmUpAndWait(); |
| final CustomTabsSession session = |
| CustomTabsTestUtils |
| .bindWithCallback(new CustomTabsCallback() { |
| @Override |
| public void onMessageChannelReady(Bundle extras) { |
| messageChannelHelper.notifyCalled(); |
| } |
| |
| @Override |
| public void onPostMessage(String message, Bundle extras) { |
| onPostMessageHelper.notifyCalled(); |
| } |
| }) |
| .session; |
| |
| Intent intent = new CustomTabsIntent.Builder(session).build().intent; |
| intent.setData(Uri.parse(url)); |
| intent.setComponent(new ComponentName( |
| InstrumentationRegistry.getTargetContext(), ChromeLauncherActivity.class)); |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| |
| mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent); |
| CriteriaHelper.pollInstrumentationThread(() -> { |
| final Tab currentTab = mCustomTabActivityTestRule.getActivity().getActivityTab(); |
| Criteria.checkThat(ChromeTabUtils.getUrlStringOnUiThread(currentTab), is(url)); |
| }); |
| |
| session.requestPostMessageChannel(Uri.parse("https://www.example.com/")); |
| |
| messageChannelHelper.waitForCallback(0); |
| onPostMessageHelper.waitForCallback(0); |
| |
| Assert.assertTrue(session.postMessage("Message", null) == CustomTabsService.RESULT_SUCCESS); |
| } |
| |
| /** |
| * Tests a postMessage request chain can start while loading a hidden tab and continue |
| * afterwards. Request sent before the hidden tab start. |
| */ |
| @Test |
| @SmallTest |
| @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE) |
| public void testPostMessageThroughHiddenTabWithRequestBeforeMayLaunchUrl() throws Exception { |
| sendPostMessageDuringHiddenTabTransition(BEFORE_MAY_LAUNCH_URL); |
| } |
| |
| /** |
| * Tests a postMessage request chain can start while loading a hidden tab and continue |
| * afterwards. Request sent after the hidden tab start and before intent launched. |
| */ |
| @Test |
| @SmallTest |
| @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE) |
| public void testPostMessageThroughHiddenTabWithRequestBeforeIntent() throws Exception { |
| sendPostMessageDuringHiddenTabTransition(BEFORE_INTENT); |
| } |
| |
| /** |
| * Tests a postMessage request chain can start while loading a hidden tab and continue |
| * afterwards. Request sent after intent received. |
| */ |
| @Test |
| @SmallTest |
| @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE) |
| public void testPostMessageThroughHiddenTabWithRequestAfterIntent() throws Exception { |
| sendPostMessageDuringHiddenTabTransition(AFTER_INTENT); |
| } |
| |
| private void sendPostMessageDuringHiddenTabTransition(int requestTime) throws Exception { |
| final CallbackHelper messageChannelHelper = new CallbackHelper(); |
| final String url = |
| mWebServer.setResponse("/test.html", TITLE_FROM_POSTMESSAGE_TO_CHANNEL, null); |
| final CustomTabsConnection connection = CustomTabsTestUtils.warmUpAndWait(); |
| |
| final CustomTabsSession session = |
| CustomTabsTestUtils |
| .bindWithCallback(new CustomTabsCallback() { |
| @Override |
| public void onMessageChannelReady(Bundle extras) { |
| messageChannelHelper.notifyCalled(); |
| } |
| }) |
| .session; |
| |
| Intent intent = new CustomTabsIntent.Builder(session).build().intent; |
| intent.setData(Uri.parse(url)); |
| intent.setComponent(new ComponentName( |
| InstrumentationRegistry.getTargetContext(), ChromeLauncherActivity.class)); |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| CustomTabsSessionToken token = CustomTabsSessionToken.getSessionTokenFromIntent(intent); |
| |
| boolean channelRequested = false; |
| String titleString = ""; |
| |
| if (requestTime == BEFORE_MAY_LAUNCH_URL) { |
| channelRequested = |
| session.requestPostMessageChannel(Uri.parse("https://www.example.com/")); |
| Assert.assertTrue(channelRequested); |
| } |
| |
| setCanUseHiddenTabForSession(connection, token, true); |
| session.mayLaunchUrl(Uri.parse(url), null, null); |
| ensureCompletedSpeculationForUrl(connection, url); |
| |
| if (requestTime == BEFORE_INTENT) { |
| channelRequested = |
| session.requestPostMessageChannel(Uri.parse("https://www.example.com/")); |
| Assert.assertTrue(channelRequested); |
| } |
| |
| if (channelRequested) { |
| messageChannelHelper.waitForCallback(0); |
| String currentMessage = "Prerendering "; |
| // Initial title update during prerender. |
| assertEquals( |
| CustomTabsService.RESULT_SUCCESS, session.postMessage(currentMessage, null)); |
| titleString = currentMessage; |
| } |
| |
| mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent); |
| |
| CriteriaHelper.pollInstrumentationThread(() -> { |
| final Tab currentTab = mCustomTabActivityTestRule.getActivity().getActivityTab(); |
| Criteria.checkThat(ChromeTabUtils.getUrlStringOnUiThread(currentTab), is(url)); |
| }); |
| |
| if (requestTime == AFTER_INTENT) { |
| channelRequested = |
| session.requestPostMessageChannel(Uri.parse("https://www.example.com/")); |
| Assert.assertTrue(channelRequested); |
| messageChannelHelper.waitForCallback(0); |
| } |
| |
| String currentMessage = "and loading "; |
| // Update title again and verify both updates went through with the channel still intact. |
| assertEquals(CustomTabsService.RESULT_SUCCESS, session.postMessage(currentMessage, null)); |
| titleString += currentMessage; |
| |
| // Request a new channel, verify it was created. |
| session.requestPostMessageChannel(Uri.parse("https://www.example.com/")); |
| messageChannelHelper.waitForCallback(1); |
| |
| String newMessage = "and refreshing"; |
| // Update title again and verify both updates went through with the channel still intact. |
| assertEquals(CustomTabsService.RESULT_SUCCESS, session.postMessage(newMessage, null)); |
| titleString += newMessage; |
| |
| final String title = titleString; |
| waitForTitle(title); |
| } |
| } |