blob: 2ae486f8ee76d36157ef29ee91349f8675af4fce [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/macros.h"
#include "base/synchronization/lock.h"
#include "base/test/scoped_feature_list.h"
#include "base/thread_annotations.h"
#include "content/browser/prerender/prerender_host.h"
#include "content/browser/prerender/prerender_host_registry.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.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_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "third_party/blink/public/common/features.h"
namespace content {
namespace {
class PrerenderBrowserTest : public ContentBrowserTest,
public testing::WithParamInterface<bool> {
public:
PrerenderBrowserTest() {
std::map<std::string, std::string> parameters;
if (IsActivationDisabled())
parameters["activation"] = "disabled";
feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kPrerender2, parameters);
}
~PrerenderBrowserTest() override = default;
void SetUpOnMainThread() override {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// Make sure the feature param is correctly set before testing.
if (IsActivationDisabled()) {
ASSERT_EQ(blink::features::kPrerender2Param.Get(),
blink::features::Prerender2ActivationMode::kDisabled);
} else {
ASSERT_EQ(blink::features::kPrerender2Param.Get(),
blink::features::Prerender2ActivationMode::kEnabled);
}
host_resolver()->AddRule("*", "127.0.0.1");
ssl_server_.AddDefaultHandlers(GetTestDataFilePath());
ssl_server_.SetSSLConfig(
net::test_server::EmbeddedTestServer::CERT_TEST_NAMES);
ssl_server_.RegisterRequestMonitor(base::BindRepeating(
&PrerenderBrowserTest::MonitorResourceRequest, base::Unretained(this)));
ASSERT_TRUE(ssl_server_.Start());
}
void TearDownOnMainThread() override {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
EXPECT_TRUE(ssl_server_.ShutdownAndWaitUntilComplete());
}
void MonitorResourceRequest(const net::test_server::HttpRequest& request) {
// This should be called on `EmbeddedTestServer::io_thread_`.
DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::UI));
base::AutoLock auto_lock(lock_);
request_count_by_path_[request.GetURL().PathForRequest()]++;
}
PrerenderHostRegistry& GetPrerenderHostRegistry() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
auto* storage_partition = static_cast<StoragePartitionImpl*>(
BrowserContext::GetDefaultStoragePartition(
shell()->web_contents()->GetBrowserContext()));
return *storage_partition->GetPrerenderHostRegistry();
}
// Adds <link rel=prerender> in the current main frame and waits until the
// completion of prerendering.
void AddPrerender(const GURL& prerendering_url) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// Start watching new web contents to be created for prerendering.
content::TestNavigationObserver observer(prerendering_url);
observer.StartWatchingNewWebContents();
// Add the link tag that will prerender the URL.
EXPECT_TRUE(ExecJs(shell()->web_contents(),
JsReplace("add_prerender($1)", prerendering_url)));
observer.Wait();
}
// Navigates to the URL and waits until the completion of navigation.
//
// Navigations that could activate a prerendered page on the multiple
// WebContents architecture (not multiple-pages architecture known as MPArch)
// should use this function instead of the NavigateToURL() test helper. This
// is because the test helper accesses the predecessor WebContents to be
// destroyed during activation and results in crashes.
// See https://crbug.com/1154501 for the MPArch migration.
void NavigateWithLocation(const GURL& url) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
content::TestNavigationObserver observer(shell()->web_contents());
// Ignore the result of ExecJs().
//
// Depending on timing, activation could destroy the current WebContents
// before ExecJs() gets a result from the frame that executed scripts. This
// results in execution failure even when the execution succeeded. See
// https://crbug.com/1156141 for details.
//
// This part will drastically be modified by the MPArch, so we take the
// approach just to ignore it instead of fixing the timing issue. When
// ExecJs() actually fails, the remaining test steps should fail, so it
// should be safe to ignore it.
ignore_result(
ExecJs(shell()->web_contents(), JsReplace("location = $1", url)));
observer.Wait();
EXPECT_EQ(shell()->web_contents()->GetURL(), url);
}
GURL GetUrl(const std::string& path) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
return ssl_server_.GetURL("a.test", path);
}
int GetRequestCount(const GURL& url) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
base::AutoLock auto_lock(lock_);
return request_count_by_path_[url.PathForRequest()];
}
bool IsActivationDisabled() const { return GetParam(); }
private:
net::test_server::EmbeddedTestServer ssl_server_{
net::test_server::EmbeddedTestServer::TYPE_HTTPS};
// Counts of requests sent to the server. Keyed by path (not by full URL)
// because the host part of the requests is translated ("a.test" to
// "127.0.0.1") before the server handles them.
// This is accessed from the UI thread and `EmbeddedTestServer::io_thread_`.
std::map<std::string, int> request_count_by_path_ GUARDED_BY(lock_);
base::test::ScopedFeatureList feature_list_;
base::Lock lock_;
};
INSTANTIATE_TEST_SUITE_P(All,
PrerenderBrowserTest,
/*disable_activation=*/testing::Bool());
IN_PROC_BROWSER_TEST_P(PrerenderBrowserTest, LinkRelPrerender) {
const GURL kInitialUrl = GetUrl("/prerender/add_prerender.html");
const GURL kPrerenderingUrl = GetUrl("/empty.html");
// Navigate to an initial page.
ASSERT_TRUE(NavigateToURL(shell(), kInitialUrl));
ASSERT_EQ(shell()->web_contents()->GetURL(), kInitialUrl);
// Add <link rel=prerender> that will prerender `kPrerenderingUrl`.
ASSERT_EQ(GetRequestCount(kPrerenderingUrl), 0);
AddPrerender(kPrerenderingUrl);
EXPECT_EQ(GetRequestCount(kPrerenderingUrl), 1);
// A prerender host for the URL should be registered.
PrerenderHostRegistry& registry = GetPrerenderHostRegistry();
EXPECT_NE(registry.FindHostByUrlForTesting(kPrerenderingUrl), nullptr);
// Activate the prerendered page.
NavigateWithLocation(kPrerenderingUrl);
// The prerender host should be consumed.
EXPECT_EQ(registry.FindHostByUrlForTesting(kPrerenderingUrl), nullptr);
if (IsActivationDisabled()) {
// Activation is disabled. The navigation should issue a request again.
EXPECT_EQ(GetRequestCount(kPrerenderingUrl), 2);
} else {
// Activating the prerendered page should not issue a request.
EXPECT_EQ(GetRequestCount(kPrerenderingUrl), 1);
}
}
IN_PROC_BROWSER_TEST_P(PrerenderBrowserTest, LinkRelPrerender_Multiple) {
const GURL kInitialUrl = GetUrl("/prerender/add_prerender.html");
const GURL kPrerenderingUrl1 = GetUrl("/empty.html?1");
const GURL kPrerenderingUrl2 = GetUrl("/empty.html?2");
// Navigate to an initial page.
ASSERT_TRUE(NavigateToURL(shell(), kInitialUrl));
ASSERT_EQ(shell()->web_contents()->GetURL(), kInitialUrl);
// Add <link rel=prerender> that will prerender `kPrerenderingUrl1` and
// `kPrerenderingUrl2`.
ASSERT_EQ(GetRequestCount(kPrerenderingUrl1), 0);
ASSERT_EQ(GetRequestCount(kPrerenderingUrl2), 0);
AddPrerender(kPrerenderingUrl1);
AddPrerender(kPrerenderingUrl2);
EXPECT_EQ(GetRequestCount(kPrerenderingUrl1), 1);
EXPECT_EQ(GetRequestCount(kPrerenderingUrl2), 1);
// Prerender hosts for `kPrerenderingUrl1` and `kPrerenderingUrl2` should be
// registered.
PrerenderHostRegistry& registry = GetPrerenderHostRegistry();
EXPECT_NE(registry.FindHostByUrlForTesting(kPrerenderingUrl1), nullptr);
EXPECT_NE(registry.FindHostByUrlForTesting(kPrerenderingUrl2), nullptr);
// Activate the prerendered page.
NavigateWithLocation(kPrerenderingUrl2);
// The prerender hosts should be consumed or destroyed for activation.
EXPECT_EQ(registry.FindHostByUrlForTesting(kPrerenderingUrl1), nullptr);
EXPECT_EQ(registry.FindHostByUrlForTesting(kPrerenderingUrl2), nullptr);
if (IsActivationDisabled()) {
// Activation is disabled. The navigation should issue a request again.
EXPECT_EQ(GetRequestCount(kPrerenderingUrl1), 1);
EXPECT_EQ(GetRequestCount(kPrerenderingUrl2), 2);
} else {
// Activating the prerendered page should not issue a request.
EXPECT_EQ(GetRequestCount(kPrerenderingUrl1), 1);
EXPECT_EQ(GetRequestCount(kPrerenderingUrl2), 1);
}
}
IN_PROC_BROWSER_TEST_P(PrerenderBrowserTest, LinkRelPrerender_Duplicate) {
const GURL kInitialUrl = GetUrl("/prerender/duplicate_prerenders.html");
const GURL kPrerenderingUrl1 = GetUrl("/empty.html?1");
const GURL kPrerenderingUrl2 = GetUrl("/empty.html?2");
// Start watching new web contents to be created for prerendering.
content::TestNavigationObserver navigation_observer1(kPrerenderingUrl1);
content::TestNavigationObserver navigation_observer2(kPrerenderingUrl2);
navigation_observer1.StartWatchingNewWebContents();
navigation_observer2.StartWatchingNewWebContents();
// Navigate to a page that initiates prerendering for `kPrerenderingUrl1`
// twice. The second prerendering request should be ignored.
ASSERT_TRUE(NavigateToURL(shell(), kInitialUrl));
// Wait until the completion of prerendering.
navigation_observer1.Wait();
navigation_observer2.Wait();
// Requests should be issued once per prerendering URL.
EXPECT_EQ(GetRequestCount(kPrerenderingUrl1), 1);
EXPECT_EQ(GetRequestCount(kPrerenderingUrl2), 1);
// Prerender hosts for `kPrerenderingUrl1` and `kPrerenderingUrl2` should be
// registered.
PrerenderHostRegistry& registry = GetPrerenderHostRegistry();
EXPECT_NE(registry.FindHostByUrlForTesting(kPrerenderingUrl1), nullptr);
EXPECT_NE(registry.FindHostByUrlForTesting(kPrerenderingUrl2), nullptr);
// Activate the prerendered page.
NavigateWithLocation(kPrerenderingUrl1);
EXPECT_EQ(shell()->web_contents()->GetURL(), kPrerenderingUrl1);
// The prerender hosts should be consumed or destroyed for activation.
EXPECT_EQ(registry.FindHostByUrlForTesting(kPrerenderingUrl1), nullptr);
EXPECT_EQ(registry.FindHostByUrlForTesting(kPrerenderingUrl2), nullptr);
if (IsActivationDisabled()) {
// Activation is disabled. The navigation should issue a request again.
EXPECT_EQ(GetRequestCount(kPrerenderingUrl1), 2);
EXPECT_EQ(GetRequestCount(kPrerenderingUrl2), 1);
} else {
// Activating the prerendered page should not issue a request.
EXPECT_EQ(GetRequestCount(kPrerenderingUrl1), 1);
EXPECT_EQ(GetRequestCount(kPrerenderingUrl2), 1);
}
}
IN_PROC_BROWSER_TEST_P(PrerenderBrowserTest, InformedRenderFrameHost) {
const GURL kInitialUrl = GetUrl("/prerender/add_prerender.html");
const GURL kPrerenderingUrl = GetUrl("/empty.html");
// Navigate to an initial page.
ASSERT_TRUE(NavigateToURL(shell(), kInitialUrl));
ASSERT_EQ(shell()->web_contents()->GetURL(), kInitialUrl);
// The initial page should not be for prerendering.
RenderFrameHostImpl* initiator_render_frame_host =
static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetMainFrame());
EXPECT_FALSE(initiator_render_frame_host->IsPrerendering());
// Add <link rel=prerender> that will prerender `kPrerenderingUrl`.
ASSERT_EQ(GetRequestCount(kPrerenderingUrl), 0);
AddPrerender(kPrerenderingUrl);
EXPECT_EQ(GetRequestCount(kPrerenderingUrl), 1);
// A prerender host for the URL should be registered.
PrerenderHostRegistry& registry = GetPrerenderHostRegistry();
PrerenderHost* prerender_host =
registry.FindHostByUrlForTesting(kPrerenderingUrl);
EXPECT_NE(prerender_host, nullptr);
// Verify the corresponding RenderFrameHostImpl knows the prerendering state.
RenderFrameHostImpl* prerendered_render_frame_host =
prerender_host->GetPrerenderedMainFrameHostForTesting();
EXPECT_TRUE(prerendered_render_frame_host->IsPrerendering());
// Activate the prerendered page.
NavigateWithLocation(kPrerenderingUrl);
if (IsActivationDisabled()) {
// Activation is disabled, so the page should newly be rendered instead
// of the prerendered page.
RenderFrameHostImpl* new_render_frame_host =
static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetMainFrame());
EXPECT_NE(prerendered_render_frame_host, new_render_frame_host);
// The new page shouldn't be in the prerendering state.
EXPECT_FALSE(new_render_frame_host->IsPrerendering());
} else {
// The prerendered page is activated. The page should no longer be in
// the prerendering state.
ASSERT_EQ(prerendered_render_frame_host,
shell()->web_contents()->GetMainFrame());
EXPECT_FALSE(prerendered_render_frame_host->IsPrerendering());
}
}
// Makes sure that activations on navigations for iframes don't happen.
IN_PROC_BROWSER_TEST_P(PrerenderBrowserTest, Activation_iFrame) {
const GURL kInitialUrl = GetUrl("/prerender/add_prerender.html");
const GURL kPrerenderingUrl = GetUrl("/empty.html");
// Navigate to an initial page.
ASSERT_TRUE(NavigateToURL(shell(), kInitialUrl));
ASSERT_EQ(shell()->web_contents()->GetURL(), kInitialUrl);
// Add <link rel=prerender> that will prerender `kPrerenderingUrl`.
ASSERT_EQ(GetRequestCount(kPrerenderingUrl), 0);
AddPrerender(kPrerenderingUrl);
EXPECT_EQ(GetRequestCount(kPrerenderingUrl), 1);
// A prerender host for the URL should be registered.
PrerenderHostRegistry& registry = GetPrerenderHostRegistry();
PrerenderHost* prerender_host =
registry.FindHostByUrlForTesting(kPrerenderingUrl);
EXPECT_TRUE(prerender_host);
// Attempt to activate the prerendered page for an iframe. This should fail
// and fallback to network request.
EXPECT_EQ("LOADED", EvalJs(shell()->web_contents(),
JsReplace("add_iframe($1)", kPrerenderingUrl)));
// Activation shouldn't happen, so the prerender host should not be consumed,
// and navigation for the iframe should issue a request again.
EXPECT_EQ(registry.FindHostByUrlForTesting(kPrerenderingUrl), prerender_host);
EXPECT_EQ(GetRequestCount(kPrerenderingUrl), 2);
}
// Makes sure that activations on navigations for pop-up windows don't happen.
IN_PROC_BROWSER_TEST_P(PrerenderBrowserTest, Activation_PopUpWindow) {
const GURL kInitialUrl = GetUrl("/prerender/add_prerender.html");
const GURL kPrerenderingUrl = GetUrl("/empty.html");
// Navigate to an initial page.
ASSERT_TRUE(NavigateToURL(shell(), kInitialUrl));
ASSERT_EQ(shell()->web_contents()->GetURL(), kInitialUrl);
// Add <link rel=prerender> that will prerender `kPrerenderingUrl`.
ASSERT_EQ(GetRequestCount(kPrerenderingUrl), 0);
AddPrerender(kPrerenderingUrl);
EXPECT_EQ(GetRequestCount(kPrerenderingUrl), 1);
// A prerender host for the URL should be registered.
PrerenderHostRegistry& registry = GetPrerenderHostRegistry();
PrerenderHost* prerender_host =
registry.FindHostByUrlForTesting(kPrerenderingUrl);
EXPECT_TRUE(prerender_host);
// Attempt to activate the prerendered page for a pop-up window. This should
// fail and fallback to network request.
EXPECT_EQ("LOADED", EvalJs(shell()->web_contents(),
JsReplace("open_window($1)", kPrerenderingUrl)));
// Activation shouldn't happen, so the prerender host should not be consumed,
// and navigation for the pop-up window should issue a request again.
EXPECT_EQ(registry.FindHostByUrlForTesting(kPrerenderingUrl), prerender_host);
EXPECT_EQ(GetRequestCount(kPrerenderingUrl), 2);
}
// Tests that back-forward history is preserved after activation.
IN_PROC_BROWSER_TEST_P(PrerenderBrowserTest, HistoryAfterActivation) {
// This test is only meaningful with activation.
if (IsActivationDisabled())
return;
const GURL kInitialUrl = GetUrl("/prerender/add_prerender.html");
const GURL kPrerenderingUrl = GetUrl("/empty.html");
// Navigate to an initial page.
ASSERT_TRUE(NavigateToURL(shell(), kInitialUrl));
// Make and activate a prerendered page.
AddPrerender(kPrerenderingUrl);
NavigateWithLocation(kPrerenderingUrl);
EXPECT_EQ(shell()->web_contents()->GetLastCommittedURL(), kPrerenderingUrl);
// Navigate back to the initial page.
content::TestNavigationObserver observer(shell()->web_contents());
shell()->GoBackOrForward(-1);
observer.Wait();
EXPECT_EQ(shell()->web_contents()->GetLastCommittedURL(), kInitialUrl);
}
// TODO(https://crbug.com/1132746): Test canceling prerendering.
// TODO(https://crbug.com/1132746): Test prerendering for 404 page, redirection,
// auth error, cross origin, etc.
} // namespace
} // namespace content