blob: 81a2389e550821ce526a4a9200751284e9632f06 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/public/test/prerender_test_util.h"
#include <tuple>
#include "base/callback_helpers.h"
#include "base/trace_event/typed_macros.h"
#include "content/browser/preloading/prerender/prerender_host_registry.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "third_party/blink/public/common/features.h"
namespace content {
namespace test {
namespace {
constexpr char kAddSpeculationRuleScript[] = R"({
const script = document.createElement('script');
script.type = 'speculationrules';
script.text = `{
"prerender": [{
"source": "list",
"urls": [$1]
}]
}`;
document.head.appendChild(script);
})";
PrerenderHostRegistry& GetPrerenderHostRegistry(WebContents* web_contents) {
EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI));
return *static_cast<WebContentsImpl*>(web_contents)
->GetPrerenderHostRegistry();
}
PrerenderHost* GetPrerenderHostById(WebContents* web_contents, int host_id) {
auto& registry = GetPrerenderHostRegistry(web_contents);
return registry.FindNonReservedHostById(host_id);
}
} // namespace
class PrerenderHostRegistryObserverImpl
: public PrerenderHostRegistry::Observer {
public:
explicit PrerenderHostRegistryObserverImpl(WebContents& web_contents) {
observation_.Observe(&GetPrerenderHostRegistry(&web_contents));
}
void WaitForTrigger(const GURL& url) {
if (triggered_.contains(url)) {
return;
}
EXPECT_FALSE(waiting_.contains(url));
base::RunLoop loop;
waiting_[url] = loop.QuitClosure();
loop.Run();
}
void NotifyOnTrigger(const GURL& url, base::OnceClosure callback) {
if (triggered_.contains(url)) {
std::move(callback).Run();
return;
}
EXPECT_FALSE(waiting_.contains(url));
waiting_[url] = std::move(callback);
}
void OnTrigger(const GURL& url) override {
auto iter = waiting_.find(url);
if (iter != waiting_.end()) {
auto callback = std::move(iter->second);
waiting_.erase(iter);
std::move(callback).Run();
} else {
EXPECT_FALSE(triggered_.contains(url))
<< "this observer doesn't yet support multiple triggers";
triggered_.insert(url);
}
}
void OnRegistryDestroyed() override {
EXPECT_TRUE(waiting_.empty());
observation_.Reset();
}
base::ScopedObservation<PrerenderHostRegistry,
PrerenderHostRegistry::Observer>
observation_{this};
base::flat_map<GURL, base::OnceClosure> waiting_;
base::flat_set<GURL> triggered_;
};
PrerenderHostRegistryObserver::PrerenderHostRegistryObserver(
WebContents& web_contents)
: impl_(std::make_unique<PrerenderHostRegistryObserverImpl>(web_contents)) {
}
PrerenderHostRegistryObserver::~PrerenderHostRegistryObserver() = default;
void PrerenderHostRegistryObserver::WaitForTrigger(const GURL& url) {
TRACE_EVENT("test", "PrerenderHostRegistryObserver::WaitForTrigger", "url",
url);
impl_->WaitForTrigger(url);
}
void PrerenderHostRegistryObserver::NotifyOnTrigger(
const GURL& url,
base::OnceClosure callback) {
TRACE_EVENT("test", "PrerenderHostRegistryObserver::NotifyOnTrigger", "url",
url);
impl_->NotifyOnTrigger(url, std::move(callback));
}
class PrerenderHostObserverImpl : public PrerenderHost::Observer {
public:
PrerenderHostObserverImpl(WebContents& web_contents, int host_id) {
PrerenderHost* host = GetPrerenderHostById(&web_contents, host_id);
DCHECK(host)
<< "A PrerenderHost with the given id does not, or no longer, exists.";
StartObserving(*host);
}
PrerenderHostObserverImpl(WebContents& web_contents, const GURL& gurl) {
registry_observer_ =
std::make_unique<PrerenderHostRegistryObserver>(web_contents);
if (PrerenderHost* host = GetPrerenderHostRegistry(&web_contents)
.FindHostByUrlForTesting(gurl)) {
StartObserving(*host);
} else {
registry_observer_->NotifyOnTrigger(
gurl,
base::BindOnce(&PrerenderHostObserverImpl::OnTrigger,
base::Unretained(this), std::ref(web_contents), gurl));
}
}
void OnActivated() override {
was_activated_ = true;
if (waiting_for_activation_)
std::move(waiting_for_activation_).Run();
}
void OnHostDestroyed() override {
observation_.Reset();
if (waiting_for_destruction_)
std::move(waiting_for_destruction_).Run();
}
void WaitForActivation() {
if (was_activated_)
return;
EXPECT_FALSE(waiting_for_activation_);
base::RunLoop loop;
waiting_for_activation_ = loop.QuitClosure();
loop.Run();
}
void WaitForDestroyed() {
if (did_observe_ && !observation_.IsObserving())
return;
EXPECT_FALSE(waiting_for_destruction_);
base::RunLoop loop;
waiting_for_destruction_ = loop.QuitClosure();
loop.Run();
}
bool was_activated() const { return was_activated_; }
private:
void OnTrigger(WebContents& web_contents, const GURL& gurl) {
PrerenderHost* host =
GetPrerenderHostRegistry(&web_contents).FindHostByUrlForTesting(gurl);
DCHECK(host) << "Attempted to trigger a prerender for [" << gurl << "] "
<< "but canceled before a PrerenderHost was created.";
StartObserving(*host);
}
void StartObserving(PrerenderHost& host) {
did_observe_ = true;
observation_.Observe(&host);
// This method may be bound and called from |registry_observer_| so don't
// add code below the reset.
registry_observer_.reset();
}
base::ScopedObservation<PrerenderHost, PrerenderHost::Observer> observation_{
this};
base::OnceClosure waiting_for_activation_;
base::OnceClosure waiting_for_destruction_;
std::unique_ptr<PrerenderHostRegistryObserver> registry_observer_;
bool was_activated_ = false;
bool did_observe_ = false;
};
PrerenderHostObserver::PrerenderHostObserver(WebContents& web_contents,
int prerender_host)
: impl_(std::make_unique<PrerenderHostObserverImpl>(web_contents,
prerender_host)) {}
PrerenderHostObserver::PrerenderHostObserver(WebContents& web_contents,
const GURL& gurl)
: impl_(std::make_unique<PrerenderHostObserverImpl>(web_contents, gurl)) {}
PrerenderHostObserver::~PrerenderHostObserver() = default;
void PrerenderHostObserver::WaitForActivation() {
TRACE_EVENT("test", "PrerenderHostObserver::WaitForActivation");
impl_->WaitForActivation();
}
void PrerenderHostObserver::WaitForDestroyed() {
TRACE_EVENT("test", "PrerenderHostObserver::WaitForDestroyed");
impl_->WaitForDestroyed();
}
bool PrerenderHostObserver::was_activated() const {
return impl_->was_activated();
}
ScopedPrerenderFeatureList::ScopedPrerenderFeatureList() {
std::vector<base::Feature> enabled_features;
#if !BUILDFLAG(IS_ANDROID)
// Prerender2 for Speculation Rules should be enabled by default on Android.
// To test the default behavior on Android, explicitly enable the feature only
// on non-Android.
//
// This is useful for preventing breakages by future changes on the complex
// flag structure. See review comments on https://crrev.com/c/3670822 for
// details.
enabled_features.push_back(blink::features::kPrerender2);
#endif
feature_list_.InitWithFeatures(enabled_features,
// Disable the memory requirement of Prerender2
// so the test can run on any bot.
{blink::features::kPrerender2MemoryControls});
}
PrerenderTestHelper::PrerenderTestHelper(const WebContents::Getter& fn)
: get_web_contents_fn_(fn) {}
PrerenderTestHelper::~PrerenderTestHelper() = default;
void PrerenderTestHelper::SetUp(
net::test_server::EmbeddedTestServer* http_server) {
EXPECT_FALSE(http_server->Started());
http_server->RegisterRequestMonitor(base::BindRepeating(
&PrerenderTestHelper::MonitorResourceRequest, base::Unretained(this)));
}
int PrerenderTestHelper::GetHostForUrl(const GURL& gurl) {
auto* host =
GetPrerenderHostRegistry(GetWebContents()).FindHostByUrlForTesting(gurl);
return host ? host->frame_tree_node_id()
: RenderFrameHost::kNoFrameTreeNodeId;
}
void PrerenderTestHelper::WaitForPrerenderLoadCompletion(int host_id) {
TRACE_EVENT("test", "PrerenderTestHelper::WaitForPrerenderLoadCompletion",
"host_id", host_id);
auto* host = GetPrerenderHostById(GetWebContents(), host_id);
ASSERT_NE(host, nullptr);
auto status = host->WaitForLoadStopForTesting();
EXPECT_EQ(status, PrerenderHost::LoadingOutcome::kLoadingCompleted);
}
// static
void PrerenderTestHelper::WaitForPrerenderLoadCompletion(
WebContents& web_contents,
const GURL& gurl) {
TRACE_EVENT("test", "PrerenderTestHelper::WaitForPrerenderLoadCompletion",
"web_contents", web_contents, "url", gurl);
PrerenderHostRegistry& registry = GetPrerenderHostRegistry(&web_contents);
PrerenderHost* host = registry.FindHostByUrlForTesting(gurl);
// Wait for the host to be created if it hasn't yet.
if (!host) {
PrerenderHostRegistryObserver observer(web_contents);
observer.WaitForTrigger(gurl);
host = registry.FindHostByUrlForTesting(gurl);
ASSERT_NE(host, nullptr);
}
auto status = host->WaitForLoadStopForTesting();
EXPECT_EQ(status, PrerenderHost::LoadingOutcome::kLoadingCompleted);
}
void PrerenderTestHelper::WaitForPrerenderLoadCompletion(const GURL& gurl) {
TRACE_EVENT("test", "PrerenderTestHelper::WaitForPrerenderLoadCompletion",
"url", gurl);
WaitForPrerenderLoadCompletion(*GetWebContents(), gurl);
}
int PrerenderTestHelper::AddPrerender(const GURL& prerendering_url) {
TRACE_EVENT("test", "PrerenderTestHelper::AddPrerender", "prerendering_url",
prerendering_url);
EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI));
AddPrerenderAsync(prerendering_url);
WaitForPrerenderLoadCompletion(prerendering_url);
int host_id = GetHostForUrl(prerendering_url);
EXPECT_NE(host_id, RenderFrameHost::kNoFrameTreeNodeId);
return host_id;
}
void PrerenderTestHelper::AddPrerenderAsync(const GURL& prerendering_url) {
TRACE_EVENT("test", "PrerenderTestHelper::AddPrerenderAsync",
"prerendering_url", prerendering_url);
EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI));
std::string script = JsReplace(kAddSpeculationRuleScript, prerendering_url);
// Have to use ExecuteJavaScriptForTests instead of ExecJs/EvalJs here,
// because some test pages have ContentSecurityPolicy and EvalJs cannot work
// with it. See the quick migration guide for EvalJs for more information.
GetWebContents()->GetPrimaryMainFrame()->ExecuteJavaScriptForTests(
base::UTF8ToUTF16(script), base::NullCallback());
}
std::unique_ptr<PrerenderHandle>
PrerenderTestHelper::AddEmbedderTriggeredPrerenderAsync(
const GURL& prerendering_url,
PrerenderTriggerType trigger_type,
const std::string& embedder_histogram_suffix,
ui::PageTransition page_transition) {
TRACE_EVENT("test", "PrerenderTestHelper::AddEmbedderTriggeredPrerenderAsync",
"prerendering_url", prerendering_url, "trigger_type",
trigger_type, "embedder_histogram_suffix",
embedder_histogram_suffix, "page_transition", page_transition);
if (!content::BrowserThread::CurrentlyOn(BrowserThread::UI))
return nullptr;
WebContents* web_contents = GetWebContents();
return web_contents->StartPrerendering(prerendering_url, trigger_type,
embedder_histogram_suffix,
page_transition, nullptr);
}
void PrerenderTestHelper::NavigatePrerenderedPage(int host_id,
const GURL& gurl) {
TRACE_EVENT("test", "PrerenderTestHelper::NavigatePrerenderedPage", "host_id",
host_id, "url", gurl);
auto* prerender_host = GetPrerenderHostById(GetWebContents(), host_id);
ASSERT_NE(prerender_host, nullptr);
RenderFrameHostImpl* prerender_render_frame_host =
prerender_host->GetPrerenderedMainFrameHost();
// Ignore the result of ExecJs().
//
// Navigation from the prerendered page could cancel prerendering and
// destroy the prerendered frame before ExecJs() gets a result from that.
// This results in execution failure even when the execution succeeded. See
// https://crbug.com/1186584 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.
std::ignore =
ExecJs(prerender_render_frame_host, JsReplace("location = $1", gurl));
}
// static
void PrerenderTestHelper::NavigatePrimaryPage(WebContents& web_contents,
const GURL& gurl) {
TRACE_EVENT("test", "PrerenderTestHelper::NavigatePrimaryPage",
"web_contents", web_contents, "url", gurl);
if (web_contents.IsLoading()) {
// Ensure that any ongoing navigation is complete prior to the construction
// of |observer| below (this navigation may complete while executing ExecJs
// machinery).
content::TestNavigationObserver initial_observer(&web_contents);
initial_observer.set_wait_event(
content::TestNavigationObserver::WaitEvent::kLoadStopped);
initial_observer.Wait();
}
EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI));
content::TestNavigationObserver observer(&web_contents);
observer.set_wait_event(
content::TestNavigationObserver::WaitEvent::kLoadStopped);
// 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.
std::ignore = ExecJs(web_contents.GetPrimaryMainFrame(),
JsReplace("location = $1", gurl));
observer.Wait();
}
void PrerenderTestHelper::NavigatePrimaryPage(const GURL& gurl) {
NavigatePrimaryPage(*GetWebContents(), gurl);
}
::testing::AssertionResult PrerenderTestHelper::VerifyPrerenderingState(
const GURL& gurl) {
PrerenderHostRegistry& registry = GetPrerenderHostRegistry(GetWebContents());
PrerenderHost* prerender_host = registry.FindHostByUrlForTesting(gurl);
RenderFrameHostImpl* prerendered_render_frame_host =
prerender_host->GetPrerenderedMainFrameHost();
std::vector<RenderFrameHost*> frames =
CollectAllRenderFrameHosts(prerendered_render_frame_host);
for (auto* frame : frames) {
auto* rfhi = static_cast<RenderFrameHostImpl*>(frame);
// All the subframes should be in LifecycleStateImpl::kPrerendering state
// before activation.
if (rfhi->lifecycle_state() !=
RenderFrameHostImpl::LifecycleStateImpl::kPrerendering) {
return ::testing::AssertionFailure() << "subframe in incorrect state";
}
if (rfhi->frame_tree()->type() != FrameTree::Type::kPrerender) {
return ::testing::AssertionFailure() << "frame tree had incorrect type";
}
}
return ::testing::AssertionSuccess();
}
RenderFrameHost* PrerenderTestHelper::GetPrerenderedMainFrameHost(int host_id) {
auto* prerender_host = GetPrerenderHostById(GetWebContents(), host_id);
EXPECT_NE(prerender_host, nullptr);
return prerender_host->GetPrerenderedMainFrameHost();
}
int PrerenderTestHelper::GetRequestCount(const GURL& url) {
EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI));
base::AutoLock auto_lock(lock_);
return request_count_by_path_[url.PathForRequest()];
}
net::test_server::HttpRequest::HeaderMap PrerenderTestHelper::GetRequestHeaders(
const GURL& url) {
EXPECT_TRUE(content::BrowserThread::CurrentlyOn(BrowserThread::UI));
base::AutoLock auto_lock(lock_);
std::string path = url.PathForRequest();
DCHECK(base::Contains(request_headers_by_path_, path)) << path;
return request_headers_by_path_[path];
}
void PrerenderTestHelper::WaitForRequest(const GURL& url, int count) {
TRACE_EVENT("test", "PrerenderTestHelper::WaitForRequest", "url", url,
"count", count);
for (;;) {
base::RunLoop run_loop;
{
base::AutoLock auto_lock(lock_);
if (request_count_by_path_[url.PathForRequest()] >= count)
return;
monitor_callback_ = run_loop.QuitClosure();
}
run_loop.Run();
}
}
void PrerenderTestHelper::MonitorResourceRequest(
const net::test_server::HttpRequest& request) {
// This should be called on `EmbeddedTestServer::io_thread_`.
EXPECT_FALSE(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
base::AutoLock auto_lock(lock_);
request_count_by_path_[request.GetURL().PathForRequest()]++;
request_headers_by_path_.emplace(request.GetURL().PathForRequest(),
request.headers);
if (monitor_callback_)
std::move(monitor_callback_).Run();
}
WebContents* PrerenderTestHelper::GetWebContents() {
return get_web_contents_fn_.Run();
}
std::string PrerenderTestHelper::GenerateHistogramName(
const std::string& histogram_base_name,
content::PrerenderTriggerType trigger_type,
const std::string& embedder_suffix) {
switch (trigger_type) {
case content::PrerenderTriggerType::kSpeculationRule:
DCHECK(embedder_suffix.empty());
return std::string(histogram_base_name) + ".SpeculationRule";
case content::PrerenderTriggerType::kEmbedder:
DCHECK(!embedder_suffix.empty());
return std::string(histogram_base_name) + ".Embedder_" + embedder_suffix;
}
NOTREACHED();
}
ScopedPrerenderWebContentsDelegate::ScopedPrerenderWebContentsDelegate(
WebContents& web_contents)
: web_contents_(web_contents.GetWeakPtr()) {
web_contents_->SetDelegate(this);
}
ScopedPrerenderWebContentsDelegate::~ScopedPrerenderWebContentsDelegate() {
if (web_contents_)
web_contents_.get()->SetDelegate(nullptr);
}
bool ScopedPrerenderWebContentsDelegate::IsPrerender2Supported(
WebContents& web_contents) {
return true;
}
} // namespace test
} // namespace content