blob: 601f9253388a9d869b8fa02c400503a91ec7554c [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <string_view>
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/renderer_host/navigation_controller_impl.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_manager_browsertest.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/content_navigation_policy.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/render_document_feature.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/network/public/cpp/features.h"
namespace content {
namespace {
// Helper function for error page navigations that makes sure that the last
// committed origin on |node| is an opaque origin with a precursor that matches
// |url|'s origin.
// Returns true if the frame has an opaque origin with the expected precursor
// information. Otherwise returns false.
bool IsOriginOpaqueAndCompatibleWithURL(FrameTreeNode* node, const GURL& url) {
url::Origin frame_origin =
node->current_frame_host()->GetLastCommittedOrigin();
if (!frame_origin.opaque()) {
LOG(ERROR) << "Frame origin was not opaque. " << frame_origin;
return false;
}
const GURL url_origin = url.DeprecatedGetOriginAsURL();
const GURL precursor_origin =
frame_origin.GetTupleOrPrecursorTupleIfOpaque().GetURL();
if (url_origin != precursor_origin) {
LOG(ERROR) << "url_origin '" << url_origin << "' != precursor_origin '"
<< precursor_origin << "'";
return false;
}
return true;
}
bool IsMainFrameOriginOpaqueAndCompatibleWithURL(Shell* shell,
const GURL& url) {
return IsOriginOpaqueAndCompatibleWithURL(
static_cast<WebContentsImpl*>(shell->web_contents())
->GetPrimaryFrameTree()
.root(),
url);
}
bool HasErrorPageSiteInfo(SiteInstance* site_instance) {
auto* site_instance_impl = static_cast<SiteInstanceImpl*>(site_instance);
return site_instance_impl->GetSiteInfo().is_error_page();
}
class BrowsingContextGroupSwapObserver : public WebContentsObserver {
public:
explicit BrowsingContextGroupSwapObserver(WebContents* web_contents)
: WebContentsObserver(web_contents) {}
void DidFinishNavigation(NavigationHandle* navigation_handle) override {
latest_swap_ = NavigationRequest::From(navigation_handle)
->browsing_context_group_swap();
}
BrowsingContextGroupSwap GetLatestBrowsingContextGroupSwap() const {
EXPECT_TRUE(latest_swap_.has_value());
return latest_swap_.value();
}
private:
std::optional<BrowsingContextGroupSwap> latest_swap_;
};
} // namespace
class ProactivelySwapBrowsingInstancesTest : public RenderFrameHostManagerTest {
public:
// Enable BFCache so that BrowsingInstance swap will happen.
ProactivelySwapBrowsingInstancesTest() {
feature_list_.InitWithFeaturesAndParameters(
GetDefaultEnabledBackForwardCacheFeaturesForTesting(),
GetDefaultDisabledBackForwardCacheFeaturesForTesting());
}
~ProactivelySwapBrowsingInstancesTest() override = default;
void ExpectTotalCount(std::string_view name,
base::HistogramBase::Count32 count) {
FetchHistogramsFromChildProcesses();
histogram_tester_.ExpectTotalCount(name, count);
}
template <typename T>
void ExpectBucketCount(std::string_view name,
T sample,
base::HistogramBase::Count32 expected_count) {
FetchHistogramsFromChildProcesses();
histogram_tester_.ExpectBucketCount(name, sample, expected_count);
}
private:
base::test::ScopedFeatureList feature_list_;
base::HistogramTester histogram_tester_;
};
// Test to ensure that the error page navigation does not change
// BrowsingInstances when window.open is present.
IN_PROC_BROWSER_TEST_P(
ProactivelySwapBrowsingInstancesTest,
ErrorPageNavigationWithWindowOpenDoesNotChangeBrowsingInstance) {
StartEmbeddedServer();
GURL url(embedded_test_server()->GetURL("/title1.html"));
GURL error_url(embedded_test_server()->GetURL("/empty.html"));
std::unique_ptr<URLLoaderInterceptor> url_interceptor =
SetupRequestFailForURL(error_url);
NavigationControllerImpl& nav_controller =
static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
// Start with a successful navigation to a document and verify there is
// only one entry in session history.
EXPECT_TRUE(NavigateToURL(shell(), url));
scoped_refptr<SiteInstance> success_site_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_EQ(1, nav_controller.GetEntryCount());
// Open a new window to ensure that we can't swap BrowsingInstances
// as we have to preserve the scripting relationship.
EXPECT_TRUE(OpenPopup(shell(), GURL(url::kAboutBlankURL), ""));
// Navigate to an url resulting in an error page and ensure a new entry
// was added to session history.
EXPECT_FALSE(NavigateToURL(shell(), error_url));
EXPECT_EQ(2, nav_controller.GetEntryCount());
scoped_refptr<SiteInstance> initial_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
EXPECT_TRUE(HasErrorPageSiteInfo(initial_instance.get()));
EXPECT_TRUE(IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), error_url));
EXPECT_TRUE(success_site_instance->IsRelatedSiteInstance(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
// Reload of the error page that still results in an error should stay in
// the related SiteInstance. Ensure this works for both browser-initiated
// reloads and renderer-initiated ones.
{
TestNavigationObserver reload_observer(shell()->web_contents());
shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false);
reload_observer.Wait();
EXPECT_FALSE(reload_observer.last_navigation_succeeded());
EXPECT_EQ(2, nav_controller.GetEntryCount());
EXPECT_TRUE(
IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), error_url));
EXPECT_TRUE(success_site_instance->IsRelatedSiteInstance(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
}
{
TestNavigationObserver reload_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), "location.reload();"));
reload_observer.Wait();
EXPECT_FALSE(reload_observer.last_navigation_succeeded());
EXPECT_EQ(2, nav_controller.GetEntryCount());
EXPECT_TRUE(
IsMainFrameOriginOpaqueAndCompatibleWithURL(shell(), error_url));
EXPECT_TRUE(success_site_instance->IsRelatedSiteInstance(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
}
// Allow the navigation to succeed and ensure the new SiteInstance
// stays related.
url_interceptor.reset();
{
TestNavigationObserver reload_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), "location.reload();"));
reload_observer.Wait();
EXPECT_TRUE(reload_observer.last_navigation_succeeded());
EXPECT_EQ(2, nav_controller.GetEntryCount());
EXPECT_TRUE(success_site_instance->IsRelatedSiteInstance(
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance()));
}
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
ReloadShouldNotChangeBrowsingInstance) {
StartEmbeddedServer();
GURL url(embedded_test_server()->GetURL("/title1.html"));
// 1) Navigate to the page.
EXPECT_TRUE(NavigateToURL(shell(), url));
scoped_refptr<SiteInstance> site_instance =
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();
// 2) Reload page.
shell()->web_contents()->GetPrimaryMainFrame()->Reload();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// Ensure that we do not change BrowsingInstances for reload.
// We should keep this even when we start swapping BrowsingInstances
// for same-site navigations.
EXPECT_EQ(site_instance,
shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance());
}
class ProactivelySwapBrowsingInstancesCrossSiteDoesNotReuseProcessTest
: public RenderFrameHostManagerTest {
public:
ProactivelySwapBrowsingInstancesCrossSiteDoesNotReuseProcessTest() {
feature_list_.InitWithFeaturesAndParameters(
GetDefaultEnabledBackForwardCacheFeaturesForTesting(),
GetDefaultDisabledBackForwardCacheFeaturesForTesting());
}
~ProactivelySwapBrowsingInstancesCrossSiteDoesNotReuseProcessTest() override =
default;
void SetUpCommandLine(base::CommandLine* command_line) override {
RenderFrameHostManagerTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kDisableSiteIsolation);
if (AreAllSitesIsolatedForTesting()) {
LOG(WARNING)
<< "This test should be run without strict site isolation. "
<< "It's going to fail when --site-per-process is specified.";
}
}
private:
base::test::ScopedFeatureList feature_list_;
};
// When we do a BrowsingInstance swap on renderer-initiated cross-site
// navigation, the current process will not be reused.
IN_PROC_BROWSER_TEST_P(
ProactivelySwapBrowsingInstancesCrossSiteDoesNotReuseProcessTest,
RendererInitiatedCrossSiteNavigationDoesNotReuseProcess) {
if (AreAllSitesIsolatedForTesting())
return;
ASSERT_TRUE(embedded_test_server()->Start());
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), a_url));
scoped_refptr<SiteInstanceImpl> a_site_instance =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Navigate to B. The navigation is document/renderer initiated.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), b_url));
scoped_refptr<SiteInstanceImpl> b_site_instance =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that A and B are in different BrowsingInstances and renderer
// processes. Without full site isolation, A and B are either both default
// SiteInstances or in the default SiteInstanceGroup of different
// BrowsingInstances.
EXPECT_FALSE(a_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
EXPECT_NE(a_site_instance->GetProcess(), b_site_instance->GetProcess());
if (ShouldUseDefaultSiteInstanceGroup()) {
EXPECT_EQ(a_site_instance->group(),
a_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
EXPECT_EQ(b_site_instance->group(),
b_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
} else {
EXPECT_TRUE(a_site_instance->IsDefaultSiteInstance());
EXPECT_TRUE(b_site_instance->IsDefaultSiteInstance());
}
}
// Different from renderer-initiated cross-site navigations, browser-initiated
// cross-site navigations do swap BrowsingInstances and processes without
// ProactivelySwapBrowsingInstance. Because of that, we shouldn't reuse the
// process for the new BrowsingInstance.
IN_PROC_BROWSER_TEST_P(
ProactivelySwapBrowsingInstancesCrossSiteDoesNotReuseProcessTest,
BrowserInitiatedCrossSiteNavigationDoesNotReuseProcess) {
if (AreAllSitesIsolatedForTesting())
return;
ASSERT_TRUE(embedded_test_server()->Start());
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), a_url));
scoped_refptr<SiteInstanceImpl> a_site_instance =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Navigate to B. The navigation is browser initiated.
EXPECT_TRUE(NavigateToURL(shell(), b_url));
scoped_refptr<SiteInstanceImpl> b_site_instance =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that A and B are in different BrowsingInstances and renderer
// processes. Without full site isolation, A and B are either both default
// SiteInstances or in the default SiteInstanceGroup of different
// BrowsingInstances.
EXPECT_FALSE(a_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
EXPECT_NE(a_site_instance->GetProcess(), b_site_instance->GetProcess());
if (ShouldUseDefaultSiteInstanceGroup()) {
EXPECT_EQ(a_site_instance->group(),
a_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
EXPECT_EQ(b_site_instance->group(),
b_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
} else {
EXPECT_TRUE(a_site_instance->IsDefaultSiteInstance());
EXPECT_TRUE(b_site_instance->IsDefaultSiteInstance());
}
}
// A test ContentBrowserClient implementation that enforce process-per-site mode
// if |should_use_process_per_site_| is true. It is used to verify that we don't
// reuse the current page's renderer process when navigating to sites that uses
// process-per-site.
class ProcessPerSiteContentBrowserClient
: public ContentBrowserTestContentBrowserClient {
public:
ProcessPerSiteContentBrowserClient() = default;
ProcessPerSiteContentBrowserClient(
const ProcessPerSiteContentBrowserClient&) = delete;
ProcessPerSiteContentBrowserClient& operator=(
const ProcessPerSiteContentBrowserClient&) = delete;
void SetShouldUseProcessPerSite(bool should_use_process_per_site) {
should_use_process_per_site_ = should_use_process_per_site;
}
bool ShouldUseProcessPerSite(BrowserContext* browser_context,
const GURL& site_url) override {
return should_use_process_per_site_;
}
private:
bool should_use_process_per_site_ = false;
};
// We should not reuse the current process on renderer-initiated navigations to
// sites that needs to use process-per-site, and should create a new process for
// the site if there isn't already a process for that site.
IN_PROC_BROWSER_TEST_P(
ProactivelySwapBrowsingInstancesCrossSiteDoesNotReuseProcessTest,
RendererInitiatedCrossSiteNavigationToProcessPerSiteURLCreatesNewProcess) {
if (AreAllSitesIsolatedForTesting())
return;
ASSERT_TRUE(embedded_test_server()->Start());
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
GURL c_url(embedded_test_server()->GetURL("c.com", "/title1.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
ProcessPerSiteContentBrowserClient content_browser_client;
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), a_url));
scoped_refptr<SiteInstanceImpl> a_site_instance =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
RenderProcessHost* original_process = a_site_instance->GetProcess();
// Navigate to B. The navigation is document/renderer initiated.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), b_url));
scoped_refptr<SiteInstanceImpl> b_site_instance =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that A and B are in different BrowsingInstances and renderer
// processes.
EXPECT_FALSE(a_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
EXPECT_NE(b_site_instance->GetProcess(), original_process);
if (ShouldUseDefaultSiteInstanceGroup()) {
EXPECT_EQ(a_site_instance->group(),
a_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
EXPECT_EQ(b_site_instance->group(),
b_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
} else {
EXPECT_TRUE(a_site_instance->IsDefaultSiteInstance());
EXPECT_TRUE(b_site_instance->IsDefaultSiteInstance());
}
// Make sure we will use process-per-site for C.
// Note this is enforcing process-per-site for all sites, which is why we turn
// it off right after the navigation to C. We might reconsider after
// crbug.com/1062211 is fixed.
content_browser_client.SetShouldUseProcessPerSite(true);
// Navigate to C. The navigation is document/renderer initiated.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), c_url));
scoped_refptr<SiteInstanceImpl> c_site_instance =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that B and C are in different BrowsingInstances and renderer
// processes.
EXPECT_FALSE(b_site_instance->IsRelatedSiteInstance(c_site_instance.get()));
if (ShouldUseDefaultSiteInstanceGroup()) {
EXPECT_EQ(c_site_instance->group(),
c_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
} else {
EXPECT_TRUE(c_site_instance->IsDefaultSiteInstance());
}
EXPECT_NE(c_site_instance->GetProcess(), original_process);
// C is using the process for C's site.
EXPECT_EQ(c_site_instance->GetProcess(),
RenderProcessHostImpl::GetSoleProcessHostForSite(
c_site_instance->GetIsolationContext(),
c_site_instance->GetSiteInfo()));
// Make sure we will not use process-per-site for B.
content_browser_client.SetShouldUseProcessPerSite(false);
// Navigate to B again. The navigation is document/renderer initiated.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), b_url));
scoped_refptr<SiteInstanceImpl> b2_site_instance =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
EXPECT_FALSE(b2_site_instance->IsRelatedSiteInstance(c_site_instance.get()));
EXPECT_FALSE(b2_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
if (ShouldUseDefaultSiteInstanceGroup()) {
EXPECT_EQ(b2_site_instance->group(),
b2_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
} else {
EXPECT_TRUE(b2_site_instance->IsDefaultSiteInstance());
}
EXPECT_NE(b2_site_instance->GetProcess(), original_process);
// Check that B and C are in different renderer processes.
EXPECT_NE(b2_site_instance->GetProcess(), c_site_instance->GetProcess());
}
// We should not reuse the current process on renderer-initiated navigations to
// sites that needs to use process-per-site, and should use the sole process for
// that site if it already exists.
IN_PROC_BROWSER_TEST_P(
ProactivelySwapBrowsingInstancesCrossSiteDoesNotReuseProcessTest,
RendererInitiatedCrossSiteNavigationToProcessPerSiteURLUsesProcessForSite) {
if (AreAllSitesIsolatedForTesting())
return;
ASSERT_TRUE(embedded_test_server()->Start());
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
ProcessPerSiteContentBrowserClient content_browser_client;
// Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), a_url));
scoped_refptr<SiteInstanceImpl> a_site_instance =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
RenderProcessHost* original_process = a_site_instance->GetProcess();
// Create a new process and set it as the sole process host for B.
scoped_refptr<SiteInstanceImpl> placeholder_b_site_instance =
SiteInstanceImpl::CreateForTesting(web_contents->GetBrowserContext(),
b_url);
RenderProcessHost* process_for_b =
RenderProcessHostImpl::CreateRenderProcessHost(
web_contents->GetBrowserContext(), placeholder_b_site_instance.get());
RenderProcessHostImpl::RegisterSoleProcessHostForSite(
process_for_b, placeholder_b_site_instance.get());
EXPECT_EQ(process_for_b,
RenderProcessHostImpl::GetSoleProcessHostForSite(
placeholder_b_site_instance->GetIsolationContext(),
placeholder_b_site_instance->GetSiteInfo()));
// Make sure we will use process-per-site for B.
content_browser_client.SetShouldUseProcessPerSite(true);
// Navigate to B. The navigation is document/renderer initiated.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), b_url));
scoped_refptr<SiteInstanceImpl> b_site_instance =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that A and B are in different BrowsingInstances but B should use the
// sole process assigned to site B.
EXPECT_FALSE(a_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
if (ShouldUseDefaultSiteInstanceGroup()) {
EXPECT_EQ(a_site_instance->group(),
a_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
EXPECT_EQ(b_site_instance->group(),
b_site_instance->DefaultSiteInstanceGroupForBrowsingInstance());
} else {
EXPECT_TRUE(a_site_instance->IsDefaultSiteInstance());
EXPECT_TRUE(b_site_instance->IsDefaultSiteInstance());
}
EXPECT_NE(b_site_instance->GetProcess(), original_process);
EXPECT_EQ(b_site_instance->GetProcess(), process_for_b);
EXPECT_EQ(b_site_instance->GetProcess(),
RenderProcessHostImpl::GetSoleProcessHostForSite(
b_site_instance->GetIsolationContext(),
b_site_instance->GetSiteInfo()));
}
// We should not reuse the current process on renderer-initiated navigations to
// sites that require a dedicated process.
IN_PROC_BROWSER_TEST_P(
ProactivelySwapBrowsingInstancesCrossSiteDoesNotReuseProcessTest,
NavigationToSiteThatRequiresDedicatedProcess) {
if (AreAllSitesIsolatedForTesting())
return;
ASSERT_TRUE(embedded_test_server()->Start());
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// The client will make sure b.com require a dedicated process.
EffectiveURLContentBrowserTestContentBrowserClient modified_client(
b_url /* url_to_modify */, b_url, true /* requires_dedicated_process */);
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), a_url));
scoped_refptr<SiteInstanceImpl> a_site_instance =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
EXPECT_FALSE(a_site_instance->RequiresDedicatedProcess());
// 2) Navigate cross-site to B. The navigation is document/renderer initiated.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), b_url));
scoped_refptr<SiteInstanceImpl> b_site_instance =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
EXPECT_TRUE(b_site_instance->RequiresDedicatedProcess());
// Check that A and B are in different BrowsingInstances and processes.
EXPECT_FALSE(a_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
EXPECT_NE(a_site_instance->GetProcess(), b_site_instance->GetProcess());
}
// We should not reuse the current process on renderer-initiated navigations to
// sites that require a dedicated process.
IN_PROC_BROWSER_TEST_P(
ProactivelySwapBrowsingInstancesCrossSiteDoesNotReuseProcessTest,
NavigationFromSiteThatRequiresDedicatedProcess) {
if (AreAllSitesIsolatedForTesting())
return;
ASSERT_TRUE(embedded_test_server()->Start());
GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL b_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// The client will make sure a.com require a dedicated process.
EffectiveURLContentBrowserTestContentBrowserClient modified_client(
a_url /* url_to_modify */, a_url, true /* requires_dedicated_process */);
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), a_url));
scoped_refptr<SiteInstanceImpl> a_site_instance =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
EXPECT_TRUE(a_site_instance->RequiresDedicatedProcess());
// 2) Navigate cross-site to B. The navigation is document/renderer initiated.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), b_url));
scoped_refptr<SiteInstanceImpl> b_site_instance =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
EXPECT_FALSE(b_site_instance->RequiresDedicatedProcess());
// Check that A and B are in different BrowsingInstances and processes.
EXPECT_FALSE(a_site_instance->IsRelatedSiteInstance(b_site_instance.get()));
EXPECT_NE(a_site_instance->GetProcess(), b_site_instance->GetProcess());
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
RendererInitiatedSameSiteNavigationReusesProcess) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// Navigate to title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Navigate to title2.html. The navigation is document/renderer initiated.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url_2));
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that title1.html and title2.html are in different BrowsingInstances
// but have the same renderer process.
EXPECT_FALSE(site_instance_1->IsRelatedSiteInstance(site_instance_2.get()));
EXPECT_EQ(site_instance_1->GetProcess(), site_instance_2->GetProcess());
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
BrowserInitiatedSameSiteNavigationReusesProcess) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// 2) Navigate to title2.html. The navigation is browser initiated.
EXPECT_TRUE(NavigateToURL(shell(), url_2));
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that title1.html and title2.html are in different BrowsingInstances
// but have the same renderer process.
EXPECT_FALSE(site_instance_1->IsRelatedSiteInstance(site_instance_2.get()));
EXPECT_EQ(site_instance_1->GetProcess(), site_instance_2->GetProcess());
// 3) Do a back navigation to title1.html.
shell()->web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(shell()->web_contents()->GetLastCommittedURL(), url_1);
scoped_refptr<SiteInstanceImpl> site_instance_1_history_nav =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// We will reuse the SiteInstance and renderer process of |site_instance_1|.
EXPECT_EQ(site_instance_1_history_nav, site_instance_1);
EXPECT_EQ(site_instance_1_history_nav->GetProcess(),
site_instance_1->GetProcess());
}
// Tests that navigations that started but haven't committed yet will be
// overridden by navigations started later if both navigations created
// speculative RFHs.
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
MultipleNavigationsStarted) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL a1_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL a2_url(embedded_test_server()->GetURL("a.com", "/title2.html"));
GURL b1_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
GURL b2_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to A1.
EXPECT_TRUE(NavigateToURL(shell(), a1_url));
auto* a1_rfh = web_contents->GetPrimaryMainFrame();
FrameTreeNode* node = a1_rfh->frame_tree_node();
scoped_refptr<SiteInstanceImpl> a1_site_instance =
static_cast<SiteInstanceImpl*>(a1_rfh->GetSiteInstance());
// 2) Start same-site navigation to A2 without committing.
TestNavigationManager navigation_a2(shell()->web_contents(), a2_url);
shell()->LoadURL(a2_url);
// Chrome in Android will use unisolated site instance, we have to
// use `WaitForResponse` since the speculative RFH won't be necessariliy
// created.
EXPECT_TRUE(navigation_a2.WaitForResponse());
// Verify that we're now navigating to |a2_url|.
EXPECT_EQ(node->navigation_request()->GetURL(), a2_url);
// We should have a speculative RFH for this navigation.
RenderFrameHostImpl* a2_speculative_rfh =
node->render_manager()->speculative_frame_host();
EXPECT_TRUE(a2_speculative_rfh);
EXPECT_NE(a1_rfh, a2_speculative_rfh);
// The speculative RFH should use a different BrowsingInstance than the
// current RFH.
scoped_refptr<SiteInstanceImpl> a2_site_instance =
static_cast<SiteInstanceImpl*>(a2_speculative_rfh->GetSiteInstance());
EXPECT_FALSE(a1_site_instance->IsRelatedSiteInstance(a2_site_instance.get()));
// 3) Start cross-site navigation to B1 without committing.
TestNavigationManager navigation_b1(shell()->web_contents(), b1_url);
shell()->LoadURL(b1_url);
EXPECT_TRUE(navigation_b1.WaitForResponse());
// Verify that we're now navigating to |b1_url|.
EXPECT_EQ(node->navigation_request()->GetURL(), b1_url);
// We should have a speculative RFH for this navigation.
RenderFrameHostImpl* b1_speculative_rfh =
node->render_manager()->speculative_frame_host();
EXPECT_TRUE(b1_speculative_rfh);
EXPECT_NE(a1_rfh, b1_speculative_rfh);
// The speculative RFH should use a different BrowsingInstance than the
// current RFH.
scoped_refptr<SiteInstanceImpl> b1_site_instance =
static_cast<SiteInstanceImpl*>(b1_speculative_rfh->GetSiteInstance());
EXPECT_FALSE(a1_site_instance->IsRelatedSiteInstance(b1_site_instance.get()));
// 4) Start same-site navigation to B2 without committing.
TestNavigationManager navigation_b2(shell()->web_contents(), b2_url);
// B2 shall reuse the speculative RFH. No RFH creation.
shell()->LoadURL(b2_url);
// Verify that we're now navigating to |b2_url|.
EXPECT_EQ(node->navigation_request()->GetURL(), b2_url);
EXPECT_TRUE(navigation_b2.WaitForResponse());
// We should have a speculative RFH for this navigation.
RenderFrameHostImpl* b2_speculative_rfh =
node->render_manager()->speculative_frame_host();
EXPECT_TRUE(b2_speculative_rfh);
EXPECT_NE(a1_rfh, b2_speculative_rfh);
// The speculative RFH should use a different BrowsingInstance than the
// current RFH.
scoped_refptr<SiteInstanceImpl> b2_site_instance =
static_cast<SiteInstanceImpl*>(b2_speculative_rfh->GetSiteInstance());
EXPECT_FALSE(a1_site_instance->IsRelatedSiteInstance(b2_site_instance.get()));
}
// Tests history same-site process reuse:
// 1. Visit A1, A2, B.
// 2. Go back to A2 (should use new process).
// 3. Go back to A1 (should reuse A2's process).
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
HistoryNavigationReusesProcess) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
GURL cross_site_url(embedded_test_server()->GetURL("b.com", "/title3.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// 2) Navigate same-site to title2.html.
EXPECT_TRUE(NavigateToURL(shell(), url_2));
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that title1.html and title2.html are in different BrowsingInstances
// but have the same renderer process.
EXPECT_FALSE(site_instance_1->IsRelatedSiteInstance(site_instance_2.get()));
EXPECT_EQ(site_instance_1->GetProcess(), site_instance_2->GetProcess());
// 3) Navigate cross-site to b.com/title3.html.
RenderFrameDeletedObserver rfh_2_deleted_observer(
web_contents->GetPrimaryMainFrame());
EXPECT_TRUE(NavigateToURL(shell(), cross_site_url));
scoped_refptr<SiteInstanceImpl> site_instance_3 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// This test expects a renderer process to eventually get deleted when we
// navigate away from the page using it, which won't happen if the page is
// kept alive in the back-forward cache. So, we should flush back-forward
// cache.
web_contents->GetController().GetBackForwardCache().Flush();
// Wait until the RFH for title2.html got deleted, and check that
// title2.html and b.com/title3.html are in different BrowsingInstances and
// renderer processes (We check this by checking whether |site_instance_2|
// still has a process or not - if it's gone then that means
// |site_instance_3| uses a different process).
rfh_2_deleted_observer.WaitUntilDeleted();
EXPECT_FALSE(site_instance_2->IsRelatedSiteInstance(site_instance_3.get()));
EXPECT_FALSE(site_instance_2->HasProcess());
// 4) Do a back navigation to title2.html.
RenderFrameDeletedObserver rfh_3_deleted_observer(
web_contents->GetPrimaryMainFrame());
shell()->web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(shell()->web_contents()->GetLastCommittedURL(), url_2);
scoped_refptr<SiteInstanceImpl> site_instance_2_history_nav =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// This test expects a renderer process to eventually get deleted when we
// navigate away from the page using it, which won't happen if the page is
// kept alive in the back-forward cache. So, we should flush back-forward
// cache.
web_contents->GetController().GetBackForwardCache().Flush();
// We should use different BrowsingInstances and processes after going back to
// title2.html because it's a cross-site navigation.
rfh_3_deleted_observer.WaitUntilDeleted();
EXPECT_FALSE(site_instance_2_history_nav->IsRelatedSiteInstance(
site_instance_3.get()));
EXPECT_FALSE(site_instance_3->HasProcess());
// 5) Do a back navigation to title1.html.
shell()->web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(shell()->web_contents()->GetLastCommittedURL(), url_1);
scoped_refptr<SiteInstanceImpl> site_instance_1_history_nav =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// We should use different BrowsingInstances for title1.html and title2.html,
// but reuse the process (because in the original navigation, the BI change
// was caused by proactive BI swap).
EXPECT_FALSE(site_instance_1_history_nav->IsRelatedSiteInstance(
site_instance_2_history_nav.get()));
EXPECT_EQ(site_instance_1_history_nav, site_instance_1);
EXPECT_TRUE(site_instance_2_history_nav->HasProcess());
EXPECT_EQ(site_instance_1_history_nav->GetProcess(),
site_instance_2_history_nav->GetProcess());
}
// Tests history same-site process reuse:
// 1. Visit A1, A2, B.
// 2. Go back two entries to A1 (should use new process).
// 3. Go forward to A2 (should reuse A1's process).
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
HistoryNavigationReusesProcess_SkipSameSiteEntry) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
GURL cross_site_url(embedded_test_server()->GetURL("b.com", "/title3.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// 2) Navigate same-site to title2.html.
EXPECT_TRUE(NavigateToURL(shell(), url_2));
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that title1.html and title2.html are in different BrowsingInstances
// but have the same renderer process.
EXPECT_FALSE(site_instance_1->IsRelatedSiteInstance(site_instance_2.get()));
EXPECT_EQ(site_instance_1->GetProcess(), site_instance_2->GetProcess());
// 3) Navigate cross-site to b.com/title3.html.
RenderFrameDeletedObserver rfh_2_deleted_observer(
web_contents->GetPrimaryMainFrame());
EXPECT_TRUE(NavigateToURL(shell(), cross_site_url));
scoped_refptr<SiteInstanceImpl> site_instance_3 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// This test expects a renderer process to eventually get deleted when we
// navigate away from the page using it, which won't happen if the page is
// kept alive in the back-forward cache. So, we should flush back-forward
// cache.
web_contents->GetController().GetBackForwardCache().Flush();
// Wait until the RFH for title2.html got deleted, and check that
// title2.html and b.com/title3.html are in different BrowsingInstances and
// renderer processes (We check this by checking whether |site_instance_2|
// still has a process or not - if it's gone then that means
// |site_instance_3| uses a different process).
rfh_2_deleted_observer.WaitUntilDeleted();
EXPECT_FALSE(site_instance_2->IsRelatedSiteInstance(site_instance_3.get()));
EXPECT_FALSE(site_instance_2->HasProcess());
// 4) Navigate back 2 entries to title1.html.
RenderFrameDeletedObserver rfh_3_deleted_observer(
web_contents->GetPrimaryMainFrame());
EXPECT_TRUE(ExecJs(shell(), "history.go(-2)"));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(shell()->web_contents()->GetLastCommittedURL(), url_1);
scoped_refptr<SiteInstanceImpl> site_instance_1_history_nav =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// This test expects a renderer process to eventually get deleted when we
// navigate away from the page using it, which won't happen if the page is
// kept alive in the back-forward cache. So, we should flush back-forward
// cache.
web_contents->GetController().GetBackForwardCache().Flush();
// We should use different BrowsingInstances and processes after going back to
// title2.html because it's a cross-site navigation.
rfh_3_deleted_observer.WaitUntilDeleted();
EXPECT_FALSE(site_instance_1_history_nav->IsRelatedSiteInstance(
site_instance_3.get()));
EXPECT_EQ(site_instance_1_history_nav, site_instance_1);
EXPECT_FALSE(site_instance_3->HasProcess());
// 5) Navigate 1 entry forward to title2.html.
EXPECT_TRUE(ExecJs(shell(), "history.go(1)"));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(shell()->web_contents()->GetLastCommittedURL(), url_2);
scoped_refptr<SiteInstanceImpl> site_instance_2_history_nav =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// We should use different BrowsingInstances for title1.html and title2.html,
// but reuse the process (because in the original navigation, the BI change
// was caused by proactive BI swap).
EXPECT_FALSE(site_instance_1_history_nav->IsRelatedSiteInstance(
site_instance_2_history_nav.get()));
EXPECT_EQ(site_instance_2_history_nav, site_instance_2);
EXPECT_TRUE(site_instance_1_history_nav->HasProcess());
EXPECT_EQ(site_instance_1_history_nav->GetProcess(),
site_instance_2_history_nav->GetProcess());
}
// Tests history same-site process reuse:
// 1. Visit A1, B, A3.
// 2. Go back two entries to A1 (should use A3's process).
// 3. Go forward to B (should use new process).
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
HistoryNavigationReusesProcess_SkipCrossSiteEntry) {
// This test expects a renderer process to eventually get deleted when we
// navigate away from the page using it, which won't happen if the page is
// kept alive in the back-forward cache. So, we should disable back-forward
// cache for this test.
DisableBackForwardCache(BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL cross_site_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
GURL url_3(embedded_test_server()->GetURL("/title3.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
RenderFrameDeletedObserver rfh_1_deleted_observer(
web_contents->GetPrimaryMainFrame());
// 2) Navigate cross-site to b.com/title2.html.
EXPECT_TRUE(NavigateToURL(shell(), cross_site_url));
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that title1.html and b.com/title2.html are in different
// BrowsingInstances and renderer processes (We check this by checking
// whether |site_instance_1| still has a process or not - if it's gone then
// that means |site_instance_2| uses a different process).
rfh_1_deleted_observer.WaitUntilDeleted();
EXPECT_FALSE(site_instance_1->IsRelatedSiteInstance(site_instance_2.get()));
EXPECT_FALSE(site_instance_1->HasProcess());
// 3) Navigate cross-site to title3.html.
RenderFrameDeletedObserver rfh_2_deleted_observer(
web_contents->GetPrimaryMainFrame());
EXPECT_TRUE(NavigateToURL(shell(), url_3));
scoped_refptr<SiteInstanceImpl> site_instance_3 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Wait until the RFH for title2.html got deleted, and check that
// b.com/title2.html and title3.html are in different BrowsingInstances and
// renderer processes (We check this by checking whether |site_instance_2|
// still has a process or not - if it's gone then that means
// |site_instance_3| uses a different process).
rfh_2_deleted_observer.WaitUntilDeleted();
EXPECT_FALSE(site_instance_2->IsRelatedSiteInstance(site_instance_3.get()));
EXPECT_FALSE(site_instance_2->HasProcess());
// 4) Navigate back 2 entries from title3.html to title1.html.
EXPECT_TRUE(ExecJs(shell(), "history.go(-2)"));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(shell()->web_contents()->GetLastCommittedURL(), url_1);
scoped_refptr<SiteInstanceImpl> site_instance_1_history_nav =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// We should use different BrowsingInstances but reuse the process when going
// back from title3.html to title1.html because it's a same-site history
// navigation.
EXPECT_FALSE(site_instance_1_history_nav->IsRelatedSiteInstance(
site_instance_3.get()));
EXPECT_EQ(site_instance_1_history_nav, site_instance_1);
EXPECT_TRUE(site_instance_3->HasProcess());
EXPECT_EQ(site_instance_1_history_nav->GetProcess(),
site_instance_3->GetProcess());
}
// Tests history same-site process reuse:
// 1. Visit A1 (which window.opens A2) then B.
// 2. Visit A3, which should use a new process (can't use A2's process).
// 2. Go back two entries to A1 (should use A2's process - the same process it
// used originally).
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
HistoryNavigationReusesProcessThatIsStillAlive) {
// This test expects a renderer process to eventually get deleted when we
// navigate away from the page using it, which won't happen if the page is
// kept alive in the back-forward cache. So, we should disable back-forward
// cache for this test.
DisableBackForwardCache(BackForwardCacheImpl::TEST_REQUIRES_NO_CACHING);
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_to_open(embedded_test_server()->GetURL("/empty.html"));
GURL cross_site_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
GURL url_3(embedded_test_server()->GetURL("/title3.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to title1.html and open a popup.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
OpenPopup(shell(), url_to_open, "foo");
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// 2) Navigate cross-site to b.com/title2.html.
EXPECT_TRUE(NavigateToURL(shell(), cross_site_url));
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that title1.html and b.com/title2.html are in different
// BrowsingInstances and renderer processes. title1.html's process will still
// be around because the window it opened earlier is still alive.
EXPECT_FALSE(site_instance_1->IsRelatedSiteInstance(site_instance_2.get()));
EXPECT_TRUE(site_instance_1->HasProcess());
EXPECT_NE(site_instance_1->GetProcess(), site_instance_2->GetProcess());
// 3) Navigate cross-site to title3.html (same-site with title1.html).
RenderFrameDeletedObserver rfh_2_deleted_observer(
web_contents->GetPrimaryMainFrame());
EXPECT_TRUE(NavigateToURL(shell(), url_3));
scoped_refptr<SiteInstanceImpl> site_instance_3 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Wait until the RFH for b.com/title2.html got deleted, and check that
// b.com/title2.html and title3.html are in different BrowsingInstances and
// renderer processes (We check this by checking whether |site_instance_2|
// still has a process or not - if it's gone then that means
// |site_instance_3| uses a different process).
rfh_2_deleted_observer.WaitUntilDeleted();
EXPECT_FALSE(site_instance_2->IsRelatedSiteInstance(site_instance_3.get()));
EXPECT_FALSE(site_instance_2->HasProcess());
// Even though title1.html and title3.html are same-site, they should use
// different processes.
EXPECT_NE(site_instance_1->GetProcess(), site_instance_3->GetProcess());
// 4) Navigate back 2 entries from title3.html to title1.html.
RenderFrameDeletedObserver rfh_3_deleted_observer(
web_contents->GetPrimaryMainFrame());
EXPECT_TRUE(ExecJs(shell(), "history.go(-2)"));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(shell()->web_contents()->GetLastCommittedURL(), url_1);
scoped_refptr<SiteInstanceImpl> site_instance_1_history_nav =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// We should use different BrowsingInstances and not reuse the process when
// going back from title3.html to title1.html because the original process
// for title1.html is still around (also title3.html shouldn't be able to
// script the window opened by title1.html).
rfh_3_deleted_observer.WaitUntilDeleted();
EXPECT_FALSE(site_instance_1_history_nav->IsRelatedSiteInstance(
site_instance_3.get()));
EXPECT_EQ(site_instance_1_history_nav, site_instance_1);
EXPECT_FALSE(site_instance_3->HasProcess());
}
// If the navigation is same-document or ends up using the same NavigationEntry
// (e.g., enter in omnibox converted to a reload), we should not do a proactive
// BrowsingInstance swap.
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
SameEntryAndSameDocumentNavigationDoesNotSwap) {
ASSERT_TRUE(embedded_test_server()->Start());
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to title1.html#foo.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/title1.html#foo")));
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// 2) Navigate from title1.html#foo to title1.html.
// This is a same-document, different-entry navigation.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that #1 and #2 are in the same SiteInstance.
EXPECT_EQ(site_instance_1, site_instance_2);
// 3) Navigate from title1.html to title1.html.
// This is a different-document, same-entry navigation.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
scoped_refptr<SiteInstanceImpl> site_instance_3 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// We should keep the same SiteInstance again.
EXPECT_EQ(site_instance_2, site_instance_3);
// 4) Navigate from title1.html to title1.html#foo.
// This is a same document navigation.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/title1.html#foo")));
scoped_refptr<SiteInstanceImpl> site_instance_4 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// We should keep the same SiteInstance again.
EXPECT_EQ(site_instance_3, site_instance_4);
// 5) Navigate from title1.html#foo to title1.html#foo.
// This is a different-document, same-entry navigation.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/title1.html#foo")));
scoped_refptr<SiteInstanceImpl> site_instance_5 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// We should keep the same SiteInstance again.
EXPECT_EQ(site_instance_4, site_instance_5);
// 6) Navigate from title1.html#foo to title1.html#bar.
// This is a same document navigation.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("/title1.html#bar")));
scoped_refptr<SiteInstanceImpl> site_instance_6 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// We should keep the same SiteInstance again.
EXPECT_EQ(site_instance_5, site_instance_6);
// 7) Do a history navigation from title1.html#bar to title1.html#foo.
// This is a same-document, different-entry history navigation.
shell()->web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
scoped_refptr<SiteInstanceImpl> site_instance_7 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// We should keep the same SiteInstance again.
EXPECT_EQ(site_instance_6, site_instance_7);
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
ReloadDoesNotSwap) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/title1.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
// 1) Navigate to title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url));
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// 2) Request a reload to happen when the controller becomes active (e.g.
// after the renderer gets killed in background on Android).
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
ASSERT_FALSE(controller.NeedsReload());
controller.SetNeedsReload();
// Set the restore type to `kRestored`, since `SetNeedsReload()` should only
// be used for session restore.
controller.GetLastCommittedEntry()->set_restore_type(RestoreType::kRestored);
ASSERT_TRUE(controller.NeedsReload());
// Set the controller as active, triggering the requested reload.
controller.SetActive(true);
EXPECT_TRUE(WaitForLoadStop(web_contents));
ASSERT_FALSE(controller.NeedsReload());
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that we're still in the same SiteInstance.
EXPECT_EQ(site_instance_1, site_instance_2);
// 3) Trigger reload using Reload().
{
TestNavigationObserver reload_observer(shell()->web_contents());
shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false);
reload_observer.Wait();
EXPECT_TRUE(reload_observer.last_navigation_succeeded());
}
scoped_refptr<SiteInstanceImpl> site_instance_3 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that we're still in the same SiteInstance.
EXPECT_EQ(site_instance_2, site_instance_3);
// 4) Trigger reload using location.reload().
{
TestNavigationObserver reload_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(shell(), "location.reload();"));
reload_observer.Wait();
EXPECT_TRUE(reload_observer.last_navigation_succeeded());
}
scoped_refptr<SiteInstanceImpl> site_instance_4 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that we're still in the same SiteInstance.
EXPECT_EQ(site_instance_3, site_instance_4);
// 5) Do a replaceState to another URL.
{
TestNavigationObserver observer(web_contents);
std::string script = "history.replaceState({}, '', '/title2.html')";
EXPECT_TRUE(ExecJs(root, script));
observer.Wait();
}
scoped_refptr<SiteInstanceImpl> site_instance_5 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that we're still in the same SiteInstance.
EXPECT_EQ(site_instance_4, site_instance_5);
// 6) Reload after a replaceState by simulating the user hitting Enter in the
// omnibox without changing the URL.
{
TestNavigationObserver observer(web_contents);
web_contents->GetController().LoadURL(web_contents->GetLastCommittedURL(),
Referrer(), ui::PAGE_TRANSITION_LINK,
std::string());
observer.Wait();
}
scoped_refptr<SiteInstanceImpl> site_instance_6 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that we're still in the same SiteInstance.
EXPECT_EQ(site_instance_5, site_instance_6);
}
// Regression test for crbug.com/340606786. This test ensures that the browser
// doesn't crash if a reload happens on a post-commit error page.
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
ReloadPostCommitErrorPage) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url(embedded_test_server()->GetURL("/title1.html"));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
// 1) Navigate to title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url));
// 2) Load post commit error page.
RenderFrameHost* root = shell()->web_contents()->GetPrimaryMainFrame();
std::string error_html = "Error page";
TestNavigationObserver error_observer(shell()->web_contents());
controller.LoadPostCommitErrorPage(root, url, error_html);
error_observer.Wait();
// 3) Request a reload to happen when the controller becomes active (e.g.
// after the renderer gets killed in background on Android).
ASSERT_FALSE(controller.NeedsReload());
controller.SetNeedsReload();
// Set the restore type to `kRestored`, since `SetNeedsReload()` should only
// be used for session restore.
controller.GetLastCommittedEntry()->set_restore_type(RestoreType::kRestored);
ASSERT_TRUE(controller.NeedsReload());
// Set the controller as active, triggering the requested reload.
controller.SetActive(true);
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// The reload should not crash.
ASSERT_FALSE(controller.NeedsReload());
}
class ProactivelySwapBrowsingInstancesTestWithoutSpeculativeRFHDelay
: public ProactivelySwapBrowsingInstancesTest {
public:
ProactivelySwapBrowsingInstancesTestWithoutSpeculativeRFHDelay() {
feature_list_for_defer_speculative_rfh_.InitAndEnableFeatureWithParameters(
features::kDeferSpeculativeRFHCreation,
{{"create_speculative_rfh_delay_ms", "0"}});
}
private:
base::test::ScopedFeatureList feature_list_for_defer_speculative_rfh_;
};
// The test disables delaying the speculative RFH creation when navigation
// starts since it checks the behavior of speculative RFH during redirection.
IN_PROC_BROWSER_TEST_P(
ProactivelySwapBrowsingInstancesTestWithoutSpeculativeRFHDelay,
SwapOnNavigationToPageThatRedirects) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
// This is a same-site URL, and will redirect to another same-site URL.
GURL same_site_redirector_url(
embedded_test_server()->GetURL("/server-redirect?" + url_2.spec()));
GURL url_3(embedded_test_server()->GetURL("/title3.html"));
// This is a cross-site URL, but will redirect to a same-site URL.
GURL cross_site_redirector_url(embedded_test_server()->GetURL(
"b.com", "/server-redirect?" + url_3.spec()));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// 2) Go to a same-site URL that will redirect us same-site to /title2.html.
EXPECT_TRUE(NavigateToURL(shell(), same_site_redirector_url,
url_2 /* expected_commit_url */));
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that we are using a different BrowsingInstance but still using the
// same renderer process.
EXPECT_FALSE(site_instance_1->IsRelatedSiteInstance(site_instance_2.get()));
EXPECT_EQ(site_instance_1->GetProcess(), site_instance_2->GetProcess());
// 3) Go to a cross-site URL that will redirect us same-site to /title3.html.
// Note that we're using a renderer-initiated navigation here. If we do a
// browser-initiated navigation, it will hit the case at crbug.com/1094147
// where we can't reuse |url_2|'s process even though |url_3| is same-site.
// TODO(crbug.com/40135315): Test with browser-initiated navigation too once
// the issue is fixed.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), cross_site_redirector_url,
url_3 /* expected_commit_url */));
scoped_refptr<SiteInstanceImpl> site_instance_3 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that we are using a different BrowsingInstance but still using the
// same renderer process.
// If site isolation is turned off, it will hit the case at crbug.com/1094147.
EXPECT_FALSE(site_instance_2->IsRelatedSiteInstance(site_instance_3.get()));
if (AreStrictSiteInstancesEnabled()) {
EXPECT_EQ(site_instance_2->GetProcess(), site_instance_3->GetProcess());
} else {
EXPECT_NE(site_instance_2->GetProcess(), site_instance_3->GetProcess());
}
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
DoNotSwapWhenReplacingHistoryEntry) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// 2) Do a location.replace() to title2.html.
{
TestNavigationObserver navigation_observer(shell()->web_contents(), 1);
EXPECT_TRUE(
ExecJs(shell(), JsReplace("window.location.replace($1)", url_2)));
navigation_observer.Wait();
EXPECT_TRUE(navigation_observer.last_navigation_succeeded());
EXPECT_EQ(shell()->web_contents()->GetLastCommittedURL(), url_2);
}
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
EXPECT_EQ(site_instance_1, site_instance_2);
}
// When we do a same-document navigation from A to A#foo then a navigation that
// does replacement (e.g., cross-process reload, or location.replace, or other
// client redirects) such that B takes the place of A#foo, we can go back to A
// with the back navigation. In this case, we might want to do a proactive BI
// swap so that page A can be bfcached.
// However, this test is currently disabled because we won't swap on any
// navigation that will replace the current history entry.
// TODO(rakina): Support this case.
IN_PROC_BROWSER_TEST_P(
ProactivelySwapBrowsingInstancesTest,
DISABLED_ShouldSwapWhenReplacingEntryWithSameDocumentPreviousEntry) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_1_anchor(embedded_test_server()->GetURL("/title1.html#foo"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// 2) Navigate same-document to title1.html#foo.
EXPECT_TRUE(NavigateToURL(shell(), url_1_anchor));
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
EXPECT_EQ(site_instance_1, site_instance_2);
// 3) Do a location.replace() to title2.html.
{
TestNavigationObserver navigation_observer(web_contents, 1);
EXPECT_TRUE(
ExecJs(shell(), JsReplace("window.location.replace($1)", url_2)));
navigation_observer.Wait();
EXPECT_TRUE(navigation_observer.last_navigation_succeeded());
EXPECT_EQ(web_contents->GetLastCommittedURL(), url_2);
}
scoped_refptr<SiteInstanceImpl> site_instance_3 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// We should swap BrowsingInstance here so that the page at url_1 (which is
// now the previous history entry) can be bfcached.
EXPECT_NE(site_instance_2, site_instance_3);
// Assert that a back navigation will go to |url_1|.
{
TestNavigationObserver navigation_observer(web_contents);
web_contents->GetController().GoBack();
navigation_observer.Wait();
EXPECT_TRUE(navigation_observer.last_navigation_succeeded());
EXPECT_EQ(web_contents->GetLastCommittedURL(), url_1);
}
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
DoNotSwapWhenRelatedContentsPresent) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate and open a new window.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
OpenPopup(shell(), url_1, "foo");
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// 2) Navigate to title2.html.
EXPECT_TRUE(NavigateToURL(shell(), url_2));
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that title1.html and title2.html are using the same SiteInstance.
EXPECT_EQ(site_instance_1, site_instance_2);
}
// We should reuse the current process on same-site navigations even if the
// site requires a dedicated process (because we are still in the same site).
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
NavigationToSiteThatRequiresDedicatedProcess) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// Make sure the site require a dedicated process.
EffectiveURLContentBrowserTestContentBrowserClient modified_client(
url_1 /* url_to_modify */, url_1, /* requires_dedicated_process */ true);
// 1) Navigate to A.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
EXPECT_TRUE(site_instance_1->RequiresDedicatedProcess());
// 2) Navigate cross-site to B. The navigation is document/renderer initiated.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url_2));
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
EXPECT_TRUE(site_instance_2->RequiresDedicatedProcess());
// Check that A and B are in different BrowsingInstances but reuse the same
// process.
EXPECT_FALSE(site_instance_1->IsRelatedSiteInstance(site_instance_2.get()));
EXPECT_EQ(site_instance_1->GetProcess(), site_instance_2->GetProcess());
}
// Tests that pagehide handlers of the old RFH are run during the commit
// of the new RFH when swapping RFH for same-site navigations due to proactive
// BrowsingInstance swap.
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
PagehideRunsDuringCommit) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/local_storage.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
RenderFrameHostImpl* main_frame_1 = web_contents->GetPrimaryMainFrame();
// Create a pagehide handler that sets item "pagehide_storage" in
// localStorage.
EXPECT_TRUE(ExecJs(
main_frame_1,
base::StringPrintf(R"(
localStorage.setItem('pagehide_storage', 'not_dispatched');
var dispatched_pagehide = false;
window.onpagehide = function(e) {
if (dispatched_pagehide) {
// We shouldn't dispatch pagehide more than once.
localStorage.setItem('pagehide_storage',
'dispatched_more_than_once');
} else if (e.persisted != %s) {
localStorage.setItem('pagehide_storage', 'wrong_persisted');
} else {
localStorage.setItem('pagehide_storage',
'dispatched_once');
}
dispatched_pagehide = true;
})",
IsBackForwardCacheEnabled() ? "true" : "false")));
// 2) Navigate to local_storage.html.
RenderFrameDeletedObserver main_frame_1_deleted_observer(main_frame_1);
EXPECT_TRUE(NavigateToURL(shell(), url_2));
// Check that title1.html and local_storage.html are in different RFHs.
RenderFrameHostImpl* main_frame_2 = web_contents->GetPrimaryMainFrame();
EXPECT_NE(main_frame_1, main_frame_2);
// Check that the value set by |main_frame_1|'s pagehide handler can be
// accessed by |main_frame_2| at load time (the first time the new page runs
// scripts), setting the |pagehide_storage_at_load| variable correctly.
EXPECT_EQ("dispatched_once",
EvalJs(main_frame_2, "pagehide_storage_at_load"));
// Check that the value for 'pagehide_storage' stays the same after
// |main_frame_2| finished loading (or |main_frame_1| deleted if bfcache is
// not enabled).
if (!IsBackForwardCacheEnabled())
main_frame_1_deleted_observer.WaitUntilDeleted();
EXPECT_EQ("dispatched_once",
EvalJs(main_frame_2, "localStorage.getItem('pagehide_storage')"));
}
// Tests that visibilitychange handlers of the old RFH are run during the commit
// of the new RFH when swapping RFH for same-site navigations due to proactive
// BrowsingInstance swap.
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
VisibilitychangeRunsDuringCommit) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/local_storage.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
RenderFrameHostImpl* main_frame_1 = web_contents->GetPrimaryMainFrame();
// Create a visibilitychange handler that sets item "visibilitychange_storage"
// localStorage.
EXPECT_TRUE(ExecJs(main_frame_1, R"(
localStorage.setItem('visibilitychange_storage', 'not_dispatched');
var dispatched_visibilitychange = false;
document.onvisibilitychange = function(e) {
if (dispatched_visibilitychange) {
// We shouldn't dispatch visibilitychange more than once.
localStorage.setItem('visibilitychange_storage',
'dispatched_more_than_once');
} else if (document.visibilityState != 'hidden') {
// We should dispatch the event when the visibilityState is
// 'hidden'.
localStorage.setItem('visibilitychange_storage', 'not_hidden');
} else {
localStorage.setItem('visibilitychange_storage',
'dispatched_once');
}
dispatched_visibilitychange = true;
})"));
// 2) Navigate to local_storage.html.
RenderFrameDeletedObserver main_frame_1_deleted_observer(main_frame_1);
EXPECT_TRUE(NavigateToURL(shell(), url_2));
// Check that title1.html and local_storage.html are in different RFHs.
RenderFrameHostImpl* main_frame_2 = web_contents->GetPrimaryMainFrame();
EXPECT_NE(main_frame_1, main_frame_2);
// Check that the value set by |main_frame_1|'s pagehide handler can be
// accessed by |main_frame_2| at load time (the first time the new page runs
// scripts), setting the |visibilitychange_storage_at_load| variable
// correctly.
EXPECT_EQ("dispatched_once",
EvalJs(main_frame_2, "visibilitychange_storage_at_load"));
// Check that the value for 'visibilitychange_storage' stays the same after
// |main_frame_2| finished loading (or |main_frame_1| got deleted, if bfcache
// is not enabled).
if (!IsBackForwardCacheEnabled())
main_frame_1_deleted_observer.WaitUntilDeleted();
EXPECT_EQ(
"dispatched_once",
EvalJs(main_frame_2, "localStorage.getItem('visibilitychange_storage')"));
}
// Tests that unload handlers of the old RFH are run during commit of the new
// RFH when swapping RFH for same-site navigations due to proactive
// BrowsingInstance swap.
// TODO(crbug.com/40142288): support this.
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
DISABLED_UnloadRunsDuringCommit) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/local_storage.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
RenderFrameHostImpl* main_frame_1 = web_contents->GetPrimaryMainFrame();
// Create an unload handler that sets item "unload_storage" in localStorage.
EXPECT_TRUE(ExecJs(main_frame_1, R"(
localStorage.setItem('unload_storage', 'not_dispatched');
var dispatched_unload = false;
window.onunload = function(e) {
if (dispatched_unload) {
// We shouldn't dispatch unload more than once.
localStorage.setItem('unload_storage',
'dispatched_more_than_once');
} else {
localStorage.setItem('unload_storage', 'dispatched_once');
}
dispatched_unload = true;
};)"));
// 2) Navigate to local_storage.html.
RenderFrameDeletedObserver main_frame_1_deleted_observer(main_frame_1);
EXPECT_TRUE(NavigateToURL(shell(), url_2));
// Check that title1.html and local_storage.html are in different RFHs.
RenderFrameHostImpl* main_frame_2 = web_contents->GetPrimaryMainFrame();
EXPECT_NE(main_frame_1, main_frame_2);
// Check that the value set by |main_frame_1|'s unload handler can be
// accessed by |main_frame_2| at load time (the first time the new page runs
// scripts), setting the |unload_storage_at_load| variable correctly.
EXPECT_EQ("dispatched_once", EvalJs(main_frame_2, "unload_storage_at_load"));
// Check that the value for 'unload_storage' stays the same after
// |main_frame_2| finished loading (or |main_frame_1| got deleted, if bfcache
// is not enabled).
if (!IsBackForwardCacheEnabled())
main_frame_1_deleted_observer.WaitUntilDeleted();
EXPECT_EQ("dispatched_once",
EvalJs(main_frame_2, "localStorage.getItem('unload_storage')"));
}
// Tests that pagehide and visibilitychange handlers of a subframe in the old
// page are run during the commit of a new main RFH when swapping RFH for
// same-site navigations due to proactive BrowsingInstance swap.
IN_PROC_BROWSER_TEST_P(
ProactivelySwapBrowsingInstancesTest,
PagehideAndVisibilitychangeInSubframesAreRunDuringCommit) {
if (IsBackForwardCacheEnabled()) {
// bfcached subframes with unload/pagehide/visibilitychange handlers will
// crash on a failed DCHECK due to crbug.com/1109742.
// TODO(crbug.com/40141877): don't skip this test when bfcache is enabled.
return;
}
ASSERT_TRUE(embedded_test_server()->Start());
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a.com(a.com)"));
GURL child_url = embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a.com()");
GURL url_2(embedded_test_server()->GetURL("a.com", "/local_storage.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to |main_url|.
EXPECT_TRUE(NavigateToURL(shell(), main_url));
RenderFrameHostImpl* main_frame_1 = web_contents->GetPrimaryMainFrame();
FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
ASSERT_EQ(1U, root->child_count());
// Check if the subframe is navigated to the correct URL.
FrameTreeNode* child = root->child_at(0);
EXPECT_EQ(child_url, child->current_url());
// Create a pagehide handler that sets item "pagehide_storage" and a
// visibilitychange handler that sets item "visibilitychange_storage" in
// localStorage in the subframe.
EXPECT_TRUE(ExecJs(
child,
base::StringPrintf(R"(
localStorage.setItem('pagehide_storage', 'not_dispatched');
var dispatched_pagehide = false;
window.onpagehide = function(e) {
if (dispatched_pagehide) {
// We shouldn't dispatch pagehide more than once.
localStorage.setItem('pagehide_storage',
'dispatched_more_than_once');
} else if (e.persisted != %s) {
localStorage.setItem('pagehide_storage', 'wrong_persisted');
} else {
localStorage.setItem('pagehide_storage', 'dispatched_once');
}
dispatched_pagehide = true;
}
localStorage.setItem('visibilitychange_storage', 'not_dispatched');
var dispatched_visibilitychange = false;
document.onvisibilitychange = function(e) {
if (dispatched_visibilitychange) {
// We shouldn't dispatch visibilitychange more than once.
localStorage.setItem('visibilitychange_storage',
'dispatched_more_than_once');
} else if (document.visibilityState != 'hidden') {
// We should dispatch the event when the visibilityState is
// 'hidden'.
localStorage.setItem('visibilitychange_storage', 'not_hidden');
} else {
localStorage.setItem('visibilitychange_storage',
'dispatched_once');
}
dispatched_visibilitychange = true;
})",
IsBackForwardCacheEnabled() ? "true" : "false")));
// 2) Navigate to local_storage.html.
RenderFrameDeletedObserver main_frame_1_deleted_observer(main_frame_1);
EXPECT_TRUE(NavigateToURL(shell(), url_2));
// Check that |main_url| and local_storage.html are in different RFHs.
RenderFrameHostImpl* main_frame_2 = web_contents->GetPrimaryMainFrame();
EXPECT_NE(main_frame_1, main_frame_2);
// Check that the value set by |child|'s pagehide and visibilitychange
// handlers can be accessed by |main_frame_2| at load time (the first time the
// new page runs scripts), setting the |pagehide_storage_at_load| and
// |visibilitychange_storage_at_load| variable correctly.
EXPECT_EQ("dispatched_once",
EvalJs(main_frame_2, "pagehide_storage_at_load"));
EXPECT_EQ("dispatched_once",
EvalJs(main_frame_2, "visibilitychange_storage_at_load"));
// Check that the value for 'pagehide_storage' and 'visibilitychange_storage'
// stays the same after |main_frame_2| finished loading (or |main_frame_1| got
// deleted, if bfcache is not enabled).
if (!IsBackForwardCacheEnabled())
main_frame_1_deleted_observer.WaitUntilDeleted();
EXPECT_EQ("dispatched_once",
EvalJs(main_frame_2, "localStorage.getItem('pagehide_storage')"));
EXPECT_EQ(
"dispatched_once",
EvalJs(main_frame_2, "localStorage.getItem('visibilitychange_storage')"));
}
// Tests that pagehide handlers of the old RFH are run during the commit
// of the new RFH when swapping RFH for same-site navigations due to proactive
// BrowsingInstance swap even if the page is already hidden (and
// visibilitychange won't run).
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesTest,
PagehideRunsDuringCommitOfHiddenPage) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_1(embedded_test_server()->GetURL("/title1.html"));
GURL url_2(embedded_test_server()->GetURL("/local_storage.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to |url_1| and hide the tab.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
RenderFrameHostImpl* main_frame_1 = web_contents->GetPrimaryMainFrame();
// We need to set it to Visibility::VISIBLE first in case this is the first
// time the visibility is updated.
web_contents->UpdateWebContentsVisibility(Visibility::VISIBLE);
web_contents->UpdateWebContentsVisibility(Visibility::HIDDEN);
EXPECT_EQ(Visibility::HIDDEN, web_contents->GetVisibility());
// Create a pagehide handler that sets item "pagehide_storage" and a
// visibilitychange handler that sets item "visibilitychange_storage" in
// localStorage.
EXPECT_TRUE(ExecJs(
main_frame_1,
base::StringPrintf(R"(
localStorage.setItem('pagehide_storage', 'not_dispatched');
var dispatched_pagehide = false;
window.onpagehide = function(e) {
if (dispatched_pagehide) {
// We shouldn't dispatch pagehide more than once.
localStorage.setItem('pagehide_storage',
'dispatched_more_than_once');
} else if (e.persisted != %s) {
localStorage.setItem('pagehide_storage', 'wrong_persisted');
} else {
localStorage.setItem('pagehide_storage', 'dispatched_once');
}
dispatched_pagehide = true;
}
localStorage.setItem('visibilitychange_storage', 'not_dispatched');
document.onvisibilitychange = function(e) {
localStorage.setItem('visibilitychange_storage',
'should_not_be_dispatched');
})",
IsBackForwardCacheEnabled() ? "true" : "false")));
// |visibilitychange_storage| should be set to its initial correct value.
EXPECT_EQ(
"not_dispatched",
EvalJs(main_frame_1, "localStorage.getItem('visibilitychange_storage')"));
// 2) Navigate to local_storage.html.
RenderFrameDeletedObserver main_frame_1_deleted_observer(main_frame_1);
EXPECT_TRUE(NavigateToURL(shell(), url_2));
// Check that |url_1| and local_storage.html are in different RFHs.
RenderFrameHostImpl* main_frame_2 = web_contents->GetPrimaryMainFrame();
EXPECT_NE(main_frame_1, main_frame_2);
// Check that the value set by |main_frame_1|'s pagehide handler can be
// accessed by |main_frame_2| at load time (the first time the new page runs
// scripts), setting the |pagehide_storage_at_load| and variable correctly.
EXPECT_EQ("dispatched_once",
EvalJs(main_frame_2, "pagehide_storage_at_load"));
// |visibilitychange_storage_at_load| should not be modified.
EXPECT_EQ("not_dispatched",
EvalJs(main_frame_2, "visibilitychange_storage_at_load"));
// Check that the value for 'pagehide_storage' and 'visibilitychange_storage'
// stays the same after |main_frame_2| finished loading (or |main_frame_1| got
// deleted, if bfcache is not enabled).
if (!IsBackForwardCacheEnabled())
main_frame_1_deleted_observer.WaitUntilDeleted();
EXPECT_EQ("dispatched_once",
EvalJs(main_frame_2, "localStorage.getItem('pagehide_storage')"));
EXPECT_EQ(
"not_dispatched",
EvalJs(main_frame_2, "localStorage.getItem('visibilitychange_storage')"));
}
// To address breakage of named window reuse across a back navigation
// when a proactive BrowsingInstance swap has happened, we offer the
// option to use an explicit opener relation for same-window navigations
// as an opt-out from proactive swaps. These tests cover cases that should (and
// should not) have the opt-out mechanism take effect.
class ProactivelySwapBrowsingInstancesOptOutTest
: public ProactivelySwapBrowsingInstancesTest {
public:
ProactivelySwapBrowsingInstancesOptOutTest() {
feature_list_.InitAndEnableFeature(
blink::features::kRelOpenerBcgDependencyHint);
}
~ProactivelySwapBrowsingInstancesOptOutTest() override = default;
protected:
void RunOptOutTest(base::FunctionRef<void()> perform_navigation,
bool expect_opt_out_applies = true) {
ASSERT_TRUE(CanSameSiteMainFrameNavigationsChangeSiteInstances());
ASSERT_TRUE(embedded_test_server()->Start());
const GURL url_1(embedded_test_server()->GetURL("/title1.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
EXPECT_TRUE(NavigateToURL(shell(), url_1));
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
BrowsingContextGroupSwapObserver swap_observer(web_contents);
perform_navigation();
EXPECT_NE(url_1,
web_contents->GetPrimaryMainFrame()->GetLastCommittedURL());
BrowsingContextGroupSwap bcg_swap =
swap_observer.GetLatestBrowsingContextGroupSwap();
EXPECT_NE(expect_opt_out_applies, bcg_swap.ShouldSwap());
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
if (expect_opt_out_applies) {
EXPECT_TRUE(
site_instance_1->IsRelatedSiteInstance(site_instance_2.get()));
EXPECT_EQ(
bcg_swap.reason(),
ShouldSwapBrowsingInstance::kNo_InitiatorRequestedNoProactiveSwap);
} else {
EXPECT_FALSE(
site_instance_1->IsRelatedSiteInstance(site_instance_2.get()));
}
}
void CreateAnchorAndNavigate(RenderFrameHost* rfh,
const GURL& url,
const std::string& target_name,
const std::string& rel) {
TestNavigationObserver nav_observer(WebContents::FromRenderFrameHost(rfh));
ExecuteScriptAsync(rfh, JsReplace(R"(
let anchor = document.createElement('a');
anchor.href = $1;
anchor.target = $2;
anchor.rel = $3;
anchor.text = 'Link';
document.body.appendChild(anchor);
anchor.click();
)",
url, target_name, rel));
nav_observer.Wait();
}
void WindowOpenNavigate(RenderFrameHost* rfh,
const GURL& url,
const std::string& target_name,
const std::string& window_features) {
TestNavigationObserver nav_observer(WebContents::FromRenderFrameHost(rfh));
ExecuteScriptAsync(rfh, JsReplace("window.open($1, $2, $3);", url,
target_name, window_features));
nav_observer.Wait();
}
void CreateFormAndSubmit(RenderFrameHost* rfh,
const GURL& action,
const std::string& target_name,
const std::string& rel) {
TestNavigationObserver nav_observer(WebContents::FromRenderFrameHost(rfh));
ExecuteScriptAsync(rfh, JsReplace(R"(
let form = document.createElement('form');
form.action = $1;
form.target = $2;
form.rel = $3;
form.method = 'POST';
document.body.appendChild(form);
form.submit();
)",
action, target_name, rel));
nav_observer.Wait();
}
FrameTreeNode* CreateIframe(RenderFrameHost* parent, const GURL& url) {
RenderFrameHostImpl* parent_impl =
static_cast<RenderFrameHostImpl*>(parent);
EXPECT_TRUE(ExecJs(parent_impl,
"var child = document.createElement('iframe');"
"document.body.appendChild(child);"));
FrameTreeNode* child =
parent_impl->child_at(parent_impl->child_count() - 1);
EXPECT_TRUE(child);
TestFrameNavigationObserver observer(child);
EXPECT_TRUE(ExecJs(parent_impl, JsReplace("child.src = $1;", url)));
observer.Wait();
return child;
}
test::FencedFrameTestHelper& fenced_frame_test_helper() {
return fenced_frame_test_helper_;
}
private:
test::FencedFrameTestHelper fenced_frame_test_helper_;
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesOptOutTest,
DoNotSwapWithAnchorRelOpener) {
RunOptOutTest([this]() {
const GURL next_url(embedded_test_server()->GetURL("/title2.html"));
CreateAnchorAndNavigate(shell()->web_contents()->GetPrimaryMainFrame(),
next_url,
/*target_name=*/"", /*rel=*/"opener");
});
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesOptOutTest,
DoNotSwapWithWindowOpener) {
RunOptOutTest([this]() {
const GURL next_url(embedded_test_server()->GetURL("/title2.html"));
WindowOpenNavigate(shell()->web_contents()->GetPrimaryMainFrame(), next_url,
/*target_name=*/"_self", /*window_features=*/"opener");
});
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesOptOutTest,
DoNotSwapWithFormRelOpener) {
RunOptOutTest([this]() {
const GURL next_url(embedded_test_server()->GetURL("/title2.html"));
CreateFormAndSubmit(shell()->web_contents()->GetPrimaryMainFrame(),
/*action=*/next_url,
/*target_name=*/"", /*rel=*/"opener");
});
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesOptOutTest,
PreventingSwapAllowsFutureScripting) {
RunOptOutTest([this]() {
const GURL next_url(embedded_test_server()->GetURL("/title2.html"));
CreateAnchorAndNavigate(shell()->web_contents()->GetPrimaryMainFrame(),
next_url,
/*target_name=*/"", /*rel=*/"opener");
});
// Beyond confirming the BrowsingInstance didn't change, we'll explicitly
// check the behaviour seen in crbug.com/40281878 to ensure that named windows
// can be reused across back navigations.
const GURL popup_url(embedded_test_server()->GetURL("/title3.html"));
ShellAddedObserver new_shell_observer;
EXPECT_TRUE(
ExecJs(shell(), JsReplace("var w = window.open($1, 'namedWindow');"
"w.previouslyOpened = true;",
popup_url)));
Shell* popup = new_shell_observer.GetShell();
EXPECT_TRUE(WaitForLoadStop(popup->web_contents()));
NavigationController& opener_controller =
shell()->web_contents()->GetController();
ASSERT_TRUE(opener_controller.CanGoBack());
TestNavigationObserver back_observer(shell()->web_contents());
opener_controller.GoBack();
back_observer.Wait();
EXPECT_EQ(true, EvalJs(shell(),
"!!window.open('', 'namedWindow').previouslyOpened;"));
// The window opened previously should be reused here, so the navigation
// should be in the same WebContents.
const GURL next_popup_url(embedded_test_server()->GetURL("/title4.html"));
TestNavigationObserver popup_nav_observer(popup->web_contents());
EXPECT_TRUE(ExecJs(
shell(), JsReplace("window.open($1, 'namedWindow');", next_popup_url)));
popup_nav_observer.Wait();
EXPECT_EQ(popup_nav_observer.last_navigation_url(), next_popup_url);
}
// Like PreventingSwapAllowsFutureScripting, but performs another navigation
// after the back navigation, to confirm that the next page can also reuse the
// opened window.
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesOptOutTest,
PreventingSwapAllowsFutureScriptingAfterAdditionalNav) {
RunOptOutTest([this]() {
const GURL next_url(embedded_test_server()->GetURL("/title2.html"));
CreateAnchorAndNavigate(shell()->web_contents()->GetPrimaryMainFrame(),
next_url,
/*target_name=*/"", /*rel=*/"opener");
});
// Beyond confirming the BrowsingInstance didn't change, we'll explicitly
// check the behaviour seen in crbug.com/40281878 to ensure that named windows
// can be reused across back navigations.
const GURL popup_url(embedded_test_server()->GetURL("/title3.html"));
ShellAddedObserver new_shell_observer;
EXPECT_TRUE(
ExecJs(shell(), JsReplace("var w = window.open($1, 'namedWindow');"
"w.previouslyOpened = true;",
popup_url)));
Shell* popup = new_shell_observer.GetShell();
EXPECT_TRUE(WaitForLoadStop(popup->web_contents()));
NavigationController& opener_controller =
shell()->web_contents()->GetController();
ASSERT_TRUE(opener_controller.CanGoBack());
TestNavigationObserver back_observer(shell()->web_contents());
opener_controller.GoBack();
back_observer.Wait();
// Navigate to another page. That page should also be able to reuse the opened
// window.
const GURL next_url2(
embedded_test_server()->GetURL("/title2.html?additionalnav"));
// We intentionally don't use rel=opener here as the usage in the original nav
// to title2.html is sufficient.
CreateAnchorAndNavigate(shell()->web_contents()->GetPrimaryMainFrame(),
next_url2,
/*target_name=*/"", /*rel=*/"");
EXPECT_EQ(true, EvalJs(shell(),
"!!window.open('', 'namedWindow').previouslyOpened;"));
// The window opened previously should be reused here, so the navigation
// should be in the same WebContents.
const GURL next_popup_url(embedded_test_server()->GetURL("/title4.html"));
TestNavigationObserver popup_nav_observer(popup->web_contents());
EXPECT_TRUE(ExecJs(
shell(), JsReplace("window.open($1, 'namedWindow');", next_popup_url)));
popup_nav_observer.Wait();
EXPECT_EQ(popup_nav_observer.last_navigation_url(), next_popup_url);
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesOptOutTest,
DoNotSwapWithAnchorRelOpenerCrossSite) {
RunOptOutTest([this]() {
const GURL cross_site_next_url(
embedded_test_server()->GetURL("b.com", "/title2.html"));
CreateAnchorAndNavigate(shell()->web_contents()->GetPrimaryMainFrame(),
cross_site_next_url,
/*target_name=*/"", /*rel=*/"opener");
});
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesOptOutTest,
DoNotSwapWithAnchorRelOpenerWithParent) {
RunOptOutTest([this]() {
const GURL iframe_url(embedded_test_server()->GetURL("/title2.html"));
const GURL next_url(embedded_test_server()->GetURL("/title3.html"));
FrameTreeNode* child = CreateIframe(
shell()->web_contents()->GetPrimaryMainFrame(), iframe_url);
CreateAnchorAndNavigate(child->current_frame_host(), next_url,
/*target_name=*/"_top", /*rel=*/"opener");
});
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesOptOutTest,
DoNotSwapWithAnchorRelOpenerInOopif) {
RunOptOutTest([this]() {
const GURL cross_site_iframe_url(
embedded_test_server()->GetURL("b.com", "/title2.html"));
const GURL next_url(embedded_test_server()->GetURL("/title3.html"));
FrameTreeNode* child = CreateIframe(
shell()->web_contents()->GetPrimaryMainFrame(), cross_site_iframe_url);
CreateAnchorAndNavigate(child->current_frame_host(), next_url,
/*target_name=*/"_top", /*rel=*/"opener");
});
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesOptOutTest,
DoNotSwapWithAnchorRelOpenerOpenURL) {
RunOptOutTest([this]() {
// This has the renderer navigate though the OpenURL method.
shell()
->web_contents()
->GetMutableRendererPrefs()
->browser_handles_all_top_level_requests = true;
shell()->web_contents()->SyncRendererPrefs();
const GURL next_url(embedded_test_server()->GetURL("/title2.html"));
CreateAnchorAndNavigate(shell()->web_contents()->GetPrimaryMainFrame(),
next_url,
/*target_name=*/"", /*rel=*/"opener");
});
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesOptOutTest,
CannotOptOutOfCoop) {
// Browsing instance swaps required by COOP cannot be bypassed by the opt out.
RunOptOutTest(
[this]() {
const GURL next_url(
embedded_test_server()->GetURL("/cross-origin-isolated.html"));
CreateAnchorAndNavigate(shell()->web_contents()->GetPrimaryMainFrame(),
next_url,
/*target_name=*/"", /*rel=*/"opener");
},
/*expect_opt_out_applies=*/false);
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesOptOutTest,
CannotOptOutFromFencedFrame) {
// _unfencedTop navigations force a browsing instance swap, which should take
// priority over the requested opt out.
RunOptOutTest(
[this]() {
const GURL fenced_frame_url(
embedded_test_server()->GetURL("/fenced_frames/title0.html"));
const GURL next_url(embedded_test_server()->GetURL("/title2.html"));
RenderFrameHost* ff_rfh = fenced_frame_test_helper().CreateFencedFrame(
shell()->web_contents()->GetPrimaryMainFrame(), fenced_frame_url,
net::OK, blink::FencedFrame::DeprecatedFencedFrameMode::kOpaqueAds);
CreateAnchorAndNavigate(ff_rfh, next_url,
/*target_name=*/"_unfencedTop",
/*rel=*/"opener");
},
/*expect_opt_out_applies=*/false);
}
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesOptOutTest,
RelOpenerAndNoopener) {
// Contradicting rel types of both opener and noopener should be treated as
// noopener.
RunOptOutTest(
[this]() {
const GURL next_url(embedded_test_server()->GetURL("/title2.html"));
CreateAnchorAndNavigate(shell()->web_contents()->GetPrimaryMainFrame(),
next_url,
/*target_name=*/"", /*rel=*/"noopener opener");
},
/*expect_opt_out_applies=*/false);
}
class ProactivelySwapBrowsingInstancesSameSiteCoopTest
: public ProactivelySwapBrowsingInstancesTest {
public:
ProactivelySwapBrowsingInstancesSameSiteCoopTest()
: https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
std::vector<base::test::FeatureRef> features;
feature_list_.InitAndEnableFeature(
network::features::kCrossOriginOpenerPolicy);
}
~ProactivelySwapBrowsingInstancesSameSiteCoopTest() override = default;
net::EmbeddedTestServer* https_server() { return &https_server_; }
private:
void SetUpOnMainThread() override {
ProactivelySwapBrowsingInstancesTest::SetUpOnMainThread();
mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(embedded_test_server()->Start());
https_server()->ServeFilesFromSourceDirectory(GetTestDataFilePath());
SetupCrossSiteRedirector(https_server());
net::test_server::RegisterDefaultHandlers(&https_server_);
ASSERT_TRUE(https_server()->Start());
}
void SetUpCommandLine(base::CommandLine* command_line) override {
ProactivelySwapBrowsingInstancesTest::SetUpCommandLine(command_line);
mock_cert_verifier_.SetUpCommandLine(command_line);
}
void SetUpInProcessBrowserTestFixture() override {
ProactivelySwapBrowsingInstancesTest::SetUpInProcessBrowserTestFixture();
mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
}
void TearDownInProcessBrowserTestFixture() override {
ProactivelySwapBrowsingInstancesTest::TearDownInProcessBrowserTestFixture();
mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
}
base::test::ScopedFeatureList feature_list_;
net::EmbeddedTestServer https_server_;
content::ContentMockCertVerifier mock_cert_verifier_;
};
// Tests history same-site process reuse:
// 1. Visit A1 (non-COOP), A2 (non-COOP, should reuse A1's process), A3 (uses
// COOP + COEP, should use new process).
// 2. Go back to A2 (should use new process).
// 3. Go back to A1 (should reuse A2's process).
IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesSameSiteCoopTest,
HistoryNavigationReusesProcess_COOP) {
GURL url_1(https_server()->GetURL("a.com", "/title1.html"));
GURL url_2(https_server()->GetURL("a.com", "/title2.html"));
GURL coop_url(
https_server()->GetURL("a.com",
"/set-header?"
"Cross-Origin-Opener-Policy: same-origin&"
"Cross-Origin-Embedder-Policy: require-corp"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// 1) Navigate to title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url_1));
EXPECT_EQ(
web_contents->GetPrimaryMainFrame()->cross_origin_opener_policy().value,
network::mojom::CrossOriginOpenerPolicyValue::kUnsafeNone);
scoped_refptr<SiteInstanceImpl> site_instance_1 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// 2) Navigate same-site to title2.html.
EXPECT_TRUE(NavigateToURL(shell(), url_2));
EXPECT_EQ(
web_contents->GetPrimaryMainFrame()->cross_origin_opener_policy().value,
network::mojom::CrossOriginOpenerPolicyValue::kUnsafeNone);
scoped_refptr<SiteInstanceImpl> site_instance_2 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// Check that title1.html and title2.html are in different BrowsingInstances
// but have the same renderer process.
EXPECT_FALSE(site_instance_1->IsRelatedSiteInstance(site_instance_2.get()));
EXPECT_EQ(site_instance_1->GetProcess(), site_instance_2->GetProcess());
// 3) Navigate same-site to a crossOriginIsolated page (uses COOP+COEP).
RenderFrameDeletedObserver rfh_2_deleted_observer(
web_contents->GetPrimaryMainFrame());
EXPECT_TRUE(NavigateToURL(shell(), coop_url));
EXPECT_EQ(
web_contents->GetPrimaryMainFrame()->cross_origin_opener_policy().value,
network::mojom::CrossOriginOpenerPolicyValue::kSameOriginPlusCoep);
// This test expects a renderer process to eventually get deleted when we
// navigate away from the page using it, which won't happen if the page is
// kept alive in the back-forward cache. So, we should flush back-forward
// cache.
web_contents->GetController().GetBackForwardCache().Flush();
// Wait until the RFH for title2.html got deleted, and check that
// title2.html and title3.html are in different BrowsingInstances and
// renderer processes (We check this by checking whether |site_instance_2|
// still has a process or not - if it's gone then that means
// |site_instance_3| uses a different process).
rfh_2_deleted_observer.WaitUntilDeleted();
scoped_refptr<SiteInstanceImpl> site_instance_3 =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
EXPECT_FALSE(site_instance_2->IsRelatedSiteInstance(site_instance_3.get()));
EXPECT_FALSE(site_instance_2->HasProcess());
EXPECT_TRUE(site_instance_3->GetProcess());
// 4) Do a back navigation to title2.html.
RenderFrameDeletedObserver rfh_3_deleted_observer(
web_contents->GetPrimaryMainFrame());
shell()->web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(shell()->web_contents()->GetLastCommittedURL(), url_2);
scoped_refptr<SiteInstanceImpl> site_instance_2_history_nav =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// This test expects a renderer process to eventually get deleted when we
// navigate away from the page using it, which won't happen if the page is
// kept alive in the back-forward cache. So, we should flush back-forward
// cache.
web_contents->GetController().GetBackForwardCache().Flush();
// We should use different BrowsingInstances and processes after going back to
// title2.html because it's transitioning from a crossOriginIsolated page
// (COOP+COEP) to a non-crossOriginIsolated page, even though the two are
// same-site.
rfh_3_deleted_observer.WaitUntilDeleted();
EXPECT_FALSE(site_instance_2_history_nav->IsRelatedSiteInstance(
site_instance_3.get()));
EXPECT_FALSE(site_instance_3->HasProcess());
// 5) Do a back navigation to title1.html.
shell()->web_contents()->GetController().GoBack();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(shell()->web_contents()->GetLastCommittedURL(), url_1);
scoped_refptr<SiteInstanceImpl> site_instance_1_history_nav =
static_cast<SiteInstanceImpl*>(
web_contents->GetPrimaryMainFrame()->GetSiteInstance());
// We should use different BrowsingInstances for title1.html and title2.html,
// but reuse the process (because in the original navigation, the BI change
// was caused by proactive BI swap).
EXPECT_FALSE(site_instance_1_history_nav->IsRelatedSiteInstance(
site_instance_2_history_nav.get()));
EXPECT_EQ(site_instance_1_history_nav, site_instance_1);
EXPECT_TRUE(site_instance_2_history_nav->HasProcess());
EXPECT_EQ(site_instance_1_history_nav->GetProcess(),
site_instance_2_history_nav->GetProcess());
}
// Tests that enable clearing window.name on on cross-site
// cross-BrowsingInstance navigations when
// ProactivelySwapBrowsingInstancesSameSite is enabled.
class ProactivelySwapBrowsingInstancesSameSiteClearWindowNameTest
: public ProactivelySwapBrowsingInstancesTest {
public:
ProactivelySwapBrowsingInstancesSameSiteClearWindowNameTest() {
feature_list_.InitAndEnableFeature(
features::kClearCrossSiteCrossBrowsingContextGroupWindowName);
}
~ProactivelySwapBrowsingInstancesSameSiteClearWindowNameTest() override =
default;
private:
base::test::ScopedFeatureList feature_list_;
};
// Verify that same-site main frame navigation that swaps BrowsingInstances
// does not clear window.name.
IN_PROC_BROWSER_TEST_P(
ProactivelySwapBrowsingInstancesSameSiteClearWindowNameTest,
NotClearWindowNameSameSite) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url_a1(embedded_test_server()->GetURL("a.com", "/title1.html"));
GURL url_a2(embedded_test_server()->GetURL("a.com", "/title2.html"));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
// Navigate to a.com/title1.html.
EXPECT_TRUE(NavigateToURL(shell(), url_a1));
// Set window.name.
EXPECT_TRUE(content::ExecJs(web_contents, "window.name='foo'"));
auto* frame_a1 = web_contents->GetPrimaryMainFrame();
EXPECT_EQ("foo", frame_a1->GetFrameName());
scoped_refptr<SiteInstance> site_instance_a1 = frame_a1->GetSiteInstance();
// Navigate to a.com/title2.html. Even though we proactively swap
// BrowsingInstances for same-site navigation as well, we should only clear
// window.name for cross-BrowsingInstance navigation that's not same-site.
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#resetBCName.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url_a2));
auto* frame_a2 = web_contents->GetPrimaryMainFrame();
// Check that title1.html and title2.html are in different BrowsingInstances.
scoped_refptr<SiteInstance> site_instance_a2 = frame_a2->GetSiteInstance();
EXPECT_FALSE(site_instance_a1->IsRelatedSiteInstance(site_instance_a2.get()));
// Window.name should not be cleared.
EXPECT_EQ("foo", frame_a2->GetFrameName());
}
INSTANTIATE_TEST_SUITE_P(All,
ProactivelySwapBrowsingInstancesTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
INSTANTIATE_TEST_SUITE_P(All,
ProactivelySwapBrowsingInstancesOptOutTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
INSTANTIATE_TEST_SUITE_P(
All,
ProactivelySwapBrowsingInstancesCrossSiteDoesNotReuseProcessTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
INSTANTIATE_TEST_SUITE_P(All,
ProactivelySwapBrowsingInstancesSameSiteCoopTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
INSTANTIATE_TEST_SUITE_P(
All,
ProactivelySwapBrowsingInstancesSameSiteClearWindowNameTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
INSTANTIATE_TEST_SUITE_P(
All,
ProactivelySwapBrowsingInstancesTestWithoutSpeculativeRFHDelay,
testing::ValuesIn(RenderDocumentFeatureLevelValues()));
} // namespace content