// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content.browser.webcontents;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcel;
import androidx.test.filters.SmallTest;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.ThreadUtils;
import org.chromium.base.process_launcher.ChildProcessConnection;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.content.browser.ChildProcessLauncherHelperImpl;
import org.chromium.content_public.browser.ChildProcessImportance;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsStatics;
import org.chromium.content_shell.Shell;
import org.chromium.content_shell_apk.ChildProcessLauncherTestUtils;
import org.chromium.content_shell_apk.ContentShellActivity;
import org.chromium.content_shell_apk.ContentShellActivityTestRule;
import java.util.List;
import java.util.concurrent.Callable;
/**
* Test various Java WebContents specific features.
* TODO(dtrainor): Add more testing for the WebContents methods.
*/
@RunWith(BaseJUnit4ClassRunner.class)
public class WebContentsTest {
@Rule
public ContentShellActivityTestRule mActivityTestRule = new ContentShellActivityTestRule();
private static final String TEST_URL_1 = "about:blank";
private static final String TEST_URL_2 = UrlUtils.encodeHtmlDataUri("<html>1</html>");
private static final String WEB_CONTENTS_KEY = "WEBCONTENTSKEY";
private static final String PARCEL_STRING_TEST_DATA = "abcdefghijklmnopqrstuvwxyz";
/**
* Check that {@link WebContents#isDestroyed()} works as expected.
* TODO(dtrainor): Test this using {@link WebContents#destroy()} instead once it is possible to
* build a {@link WebContents} directly in the content/ layer.
*/
@Test
@SmallTest
public void testWebContentsIsDestroyedMethod() {
final ContentShellActivity activity =
mActivityTestRule.launchContentShellWithUrl(TEST_URL_1);
mActivityTestRule.waitForActiveShellToBeDoneLoading();
WebContents webContents = activity.getActiveWebContents();
Assert.assertFalse(
"WebContents incorrectly marked as destroyed", isWebContentsDestroyed(webContents));
// Launch a new shell.
Shell originalShell = activity.getActiveShell();
mActivityTestRule.loadNewShell(TEST_URL_1);
Assert.assertNotSame("New shell not created", activity.getActiveShell(), originalShell);
Assert.assertTrue(
"WebContents incorrectly marked as not destroyed",
isWebContentsDestroyed(webContents));
}
/**
* Check that it is possible to serialize and deserialize a WebContents object through Parcels.
*
*/
@Test
@SmallTest
public void testWebContentsSerializeDeserializeInParcel() {
mActivityTestRule.launchContentShellWithUrl(TEST_URL_1);
mActivityTestRule.waitForActiveShellToBeDoneLoading();
WebContents webContents = mActivityTestRule.getWebContents();
Parcel parcel = Parcel.obtain();
try {
// Serialize the WebContents.
parcel.writeParcelable(webContents, 0);
// Read back the WebContents.
parcel.setDataPosition(0);
WebContents deserializedWebContents =
parcel.readParcelable(WebContents.class.getClassLoader());
// Make sure they're equal.
Assert.assertEquals(
"Deserialized object does not match", webContents, deserializedWebContents);
} finally {
parcel.recycle();
}
}
/**
* Check that it is possible to serialize and deserialize a WebContents object through Bundles.
*/
@Test
@SmallTest
// TODO(crbug.com/40479664): Fix this properly.
@SuppressLint("ParcelClassLoader")
public void testWebContentsSerializeDeserializeInBundle() {
mActivityTestRule.launchContentShellWithUrl(TEST_URL_1);
mActivityTestRule.waitForActiveShellToBeDoneLoading();
WebContents webContents = mActivityTestRule.getWebContents();
// Use a parcel to force the Bundle to actually serialize and deserialize, otherwise it can
// cache the WebContents object.
Parcel parcel = Parcel.obtain();
try {
// Create a bundle and put the WebContents in it.
Bundle bundle = new Bundle();
bundle.putParcelable(WEB_CONTENTS_KEY, webContents);
// Serialize the Bundle.
parcel.writeBundle(bundle);
// Read back the Bundle.
parcel.setDataPosition(0);
Bundle deserializedBundle = parcel.readBundle();
// Read back the WebContents.
deserializedBundle.setClassLoader(WebContents.class.getClassLoader());
WebContents deserializedWebContents =
deserializedBundle.getParcelable(WEB_CONTENTS_KEY);
// Make sure they're equal.
Assert.assertEquals(
"Deserialized object does not match", webContents, deserializedWebContents);
} finally {
parcel.recycle();
}
}
/**
* Check that it is possible to serialize and deserialize a WebContents object through Intents.
*/
@Test
@SmallTest
// TODO(crbug.com/40479664): Fix this properly.
@SuppressLint("ParcelClassLoader")
public void testWebContentsSerializeDeserializeInIntent() {
mActivityTestRule.launchContentShellWithUrl(TEST_URL_1);
mActivityTestRule.waitForActiveShellToBeDoneLoading();
WebContents webContents = mActivityTestRule.getWebContents();
// Use a parcel to force the Intent to actually serialize and deserialize, otherwise it can
// cache the WebContents object.
Parcel parcel = Parcel.obtain();
try {
// Create an Intent and put the WebContents in it.
Intent intent = new Intent();
intent.putExtra(WEB_CONTENTS_KEY, webContents);
// Serialize the Intent
parcel.writeParcelable(intent, 0);
// Read back the Intent.
parcel.setDataPosition(0);
Intent deserializedIntent = parcel.readParcelable(null);
// Read back the WebContents.
deserializedIntent.setExtrasClassLoader(WebContents.class.getClassLoader());
WebContents deserializedWebContents =
(WebContents) deserializedIntent.getParcelableExtra(WEB_CONTENTS_KEY);
// Make sure they're equal.
Assert.assertEquals(
"Deserialized object does not match", webContents, deserializedWebContents);
} finally {
parcel.recycle();
}
}
/**
* Check that attempting to deserialize a WebContents object from a Parcel from another process
* instance fails.
*/
@Test
@SmallTest
public void testWebContentsFailDeserializationAcrossProcessBoundary() {
mActivityTestRule.launchContentShellWithUrl(TEST_URL_1);
mActivityTestRule.waitForActiveShellToBeDoneLoading();
WebContents webContents = mActivityTestRule.getWebContents();
Parcel parcel = Parcel.obtain();
try {
// Serialize the WebContents.
parcel.writeParcelable(webContents, 0);
// Invalidate all serialized WebContents.
WebContentsImpl.invalidateSerializedWebContentsForTesting();
// Try to read back the WebContents.
parcel.setDataPosition(0);
WebContents deserializedWebContents =
parcel.readParcelable(WebContents.class.getClassLoader());
// Make sure we weren't able to deserialize the WebContents.
Assert.assertNull("Unexpectedly deserialized a WebContents", deserializedWebContents);
} finally {
parcel.recycle();
}
}
/**
* Check that serializing a destroyed WebContents always results in a null deserialized
* WebContents.
*/
@Test
@SmallTest
public void testSerializingADestroyedWebContentsDoesNotDeserialize() {
ContentShellActivity activity = mActivityTestRule.launchContentShellWithUrl(TEST_URL_1);
mActivityTestRule.waitForActiveShellToBeDoneLoading();
WebContents webContents = activity.getActiveWebContents();
mActivityTestRule.loadNewShell(TEST_URL_1);
Assert.assertTrue("WebContents not destroyed", isWebContentsDestroyed(webContents));
Parcel parcel = Parcel.obtain();
try {
// Serialize the WebContents.
parcel.writeParcelable(webContents, 0);
// Try to read back the WebContents.
parcel.setDataPosition(0);
WebContents deserializedWebContents =
parcel.readParcelable(WebContents.class.getClassLoader());
// Make sure we weren't able to deserialize the WebContents.
Assert.assertNull(
"Unexpectedly deserialized a destroyed WebContents", deserializedWebContents);
} finally {
parcel.recycle();
}
}
/**
* Check that destroying a WebContents after serializing it always results in a null
* deserialized WebContents.
*/
@Test
@SmallTest
public void testDestroyingAWebContentsAfterSerializingDoesNotDeserialize() {
ContentShellActivity activity = mActivityTestRule.launchContentShellWithUrl(TEST_URL_1);
mActivityTestRule.waitForActiveShellToBeDoneLoading();
WebContents webContents = activity.getActiveWebContents();
Parcel parcel = Parcel.obtain();
try {
// Serialize the WebContents.
parcel.writeParcelable(webContents, 0);
// Destroy the WebContents.
mActivityTestRule.loadNewShell(TEST_URL_1);
Assert.assertTrue("WebContents not destroyed", isWebContentsDestroyed(webContents));
// Try to read back the WebContents.
parcel.setDataPosition(0);
WebContents deserializedWebContents =
parcel.readParcelable(WebContents.class.getClassLoader());
// Make sure we weren't able to deserialize the WebContents.
Assert.assertNull(
"Unexpectedly deserialized a destroyed WebContents", deserializedWebContents);
} finally {
parcel.recycle();
}
}
/**
* Check that failing a WebContents deserialization doesn't corrupt subsequent data in the
* Parcel.
*/
@Test
@SmallTest
public void testFailedDeserializationDoesntCorruptParcel() {
mActivityTestRule.launchContentShellWithUrl(TEST_URL_1);
mActivityTestRule.waitForActiveShellToBeDoneLoading();
WebContents webContents = mActivityTestRule.getWebContents();
Parcel parcel = Parcel.obtain();
try {
// Serialize the WebContents.
parcel.writeParcelable(webContents, 0);
// Serialize a String after the WebContents.
parcel.writeString(PARCEL_STRING_TEST_DATA);
// Invalidate all serialized WebContents.
WebContentsImpl.invalidateSerializedWebContentsForTesting();
// Try to read back the WebContents.
parcel.setDataPosition(0);
WebContents deserializedWebContents =
parcel.readParcelable(WebContents.class.getClassLoader());
// Make sure we weren't able to deserialize the WebContents.
Assert.assertNull("Unexpectedly deserialized a WebContents", deserializedWebContents);
// Make sure we can properly deserialize the String after the WebContents.
Assert.assertEquals(
"Failing to read the WebContents corrupted the parcel",
PARCEL_STRING_TEST_DATA,
parcel.readString());
} finally {
parcel.recycle();
}
}
/**
* Check that the main frame associated with the WebContents is not null
* and corresponds with the test URL.
*
*/
@Test
@SmallTest
public void testWebContentsMainFrame() {
final ContentShellActivity activity =
mActivityTestRule.launchContentShellWithUrl(TEST_URL_2);
mActivityTestRule.waitForActiveShellToBeDoneLoading();
final WebContents webContents = activity.getActiveWebContents();
PostTask.postTask(
TaskTraits.UI_DEFAULT,
new Runnable() {
@Override
public void run() {
RenderFrameHost frameHost = webContents.getMainFrame();
Assert.assertNotNull(frameHost);
Assert.assertEquals(
"RenderFrameHost has incorrect last committed URL",
TEST_URL_2,
frameHost.getLastCommittedURL().getSpec());
WebContents associatedWebContents =
WebContentsStatics.fromRenderFrameHost(frameHost);
Assert.assertEquals(
"RenderFrameHost associated with different WebContents",
webContents,
associatedWebContents);
}
});
}
@Test
@SmallTest
public void testWebContentsGetAllRenderFrameHosts() {
String testUrl =
UrlUtils.encodeHtmlDataUri(
"<html><body>"
+ " <iframe srcdoc='<body>frame1</body>'></iframe>"
+ " <iframe srcdoc='<body>frame2</body>'></iframe>"
+ "</body></html>");
final ContentShellActivity activity = mActivityTestRule.launchContentShellWithUrl(testUrl);
mActivityTestRule.waitForActiveShellToBeDoneLoading();
final WebContentsImpl webContents = ((WebContentsImpl) activity.getActiveWebContents());
ThreadUtils.runOnUiThreadBlocking(
() -> {
List<RenderFrameHost> frames = webContents.getAllRenderFrameHosts();
Assert.assertEquals(3, frames.size());
});
}
private ChildProcessConnection getSandboxedChildProcessConnection() {
Callable<ChildProcessConnection> getConnectionCallable =
() -> {
for (ChildProcessLauncherHelperImpl process :
ChildProcessLauncherHelperImpl.getAllProcessesForTesting().values()) {
ChildProcessConnection connection = process.getChildProcessConnection();
if (connection.getServiceName().getClassName().indexOf("Sandbox") != -1) {
return connection;
}
}
Assert.assertTrue(false);
return null;
};
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(getConnectionCallable);
}
@Test
@SmallTest
// This test may run with --site-per-process or AndroidWarmUpSpareRendererWithTimeout,
// which also enables a feature to maintain a spare renderer process.
// The test expects only one renderer process and may incorrectly check its
// assertions on the spare process instead, so disable it.
@CommandLineFlags.Add({
"disable-features=SpareRendererForSitePerProcess,AndroidWarmUpSpareRendererWithTimeout"
})
public void testChildProcessImportance() {
final ContentShellActivity activity =
mActivityTestRule.launchContentShellWithUrl(TEST_URL_1);
mActivityTestRule.waitForActiveShellToBeDoneLoading();
final WebContents webContents = activity.getActiveWebContents();
// Make sure visibility do not affect bindings.
ThreadUtils.runOnUiThreadBlocking(() -> webContents.onHide());
final ChildProcessConnection connection = getSandboxedChildProcessConnection();
// Need to poll here because there is an intentional delay for removing binding.
CriteriaHelper.pollInstrumentationThread(
() -> {
return ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
() -> !connection.isVisibleBindingBound());
},
"Failed to remove moderate binding");
ThreadUtils.runOnUiThreadBlocking(
() -> webContents.setImportance(ChildProcessImportance.MODERATE));
ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(
() -> Assert.assertTrue(connection.isVisibleBindingBound()));
}
@Test
@SmallTest
// This test may run with --site-per-process or AndroidWarmUpSpareRendererWithTimeout,
// which also enables a feature to maintain a spare renderer process.
// The test expects only one renderer process and may incorrectly check its
// assertions on the spare process instead, so disable it.
@CommandLineFlags.Add({
"disable-features=SpareRendererForSitePerProcess,AndroidWarmUpSpareRendererWithTimeout"
})
public void testVisibilityControlsBinding() {
final ContentShellActivity activity =
mActivityTestRule.launchContentShellWithUrl(TEST_URL_1);
mActivityTestRule.waitForActiveShellToBeDoneLoading();
final WebContents webContents = activity.getActiveWebContents();
final ChildProcessConnection connection = getSandboxedChildProcessConnection();
ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(
() -> Assert.assertTrue(connection.isStrongBindingBound()));
ThreadUtils.runOnUiThreadBlocking(() -> webContents.onHide());
CriteriaHelper.pollInstrumentationThread(
() ->
ChildProcessLauncherTestUtils.runOnLauncherAndGetResult(
() -> !connection.isStrongBindingBound()),
"Failed to remove strong binding");
ThreadUtils.runOnUiThreadBlocking(() -> webContents.onShow());
ChildProcessLauncherTestUtils.runOnLauncherThreadBlocking(
() -> Assert.assertTrue(connection.isStrongBindingBound()));
}
private boolean isWebContentsDestroyed(final WebContents webContents) {
return ThreadUtils.runOnUiThreadBlocking(
new Callable<Boolean>() {
@Override
public Boolean call() {
return webContents.isDestroyed();
}
});
}
}