blob: 19570cda284cf8c94ec9bdc93b84ce43b1cd916b [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 "content/browser/preloading/preloading_decider.h"
#include <vector>
#include "base/test/scoped_feature_list.h"
#include "content/browser/preloading/prefetch/prefetch_document_manager.h"
#include "content/browser/preloading/prefetch/prefetch_features.h"
#include "content/browser/preloading/prefetch/prefetch_service.h"
#include "content/browser/preloading/prefetcher.h"
#include "content/browser/preloading/prerenderer.h"
#include "content/public/browser/anchor_element_preconnect_delegate.h"
#include "content/public/common/content_client.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_renderer_host.h"
#include "content/test/test_content_browser_client.h"
#include "content/test/test_web_contents.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace content {
namespace {
class PrerenderWebContentsDelegate : public WebContentsDelegate {
public:
PrerenderWebContentsDelegate() = default;
bool IsPrerender2Supported(WebContents& web_contents) override {
return true;
}
};
class MockAnchorElementPreconnector : public AnchorElementPreconnectDelegate {
public:
explicit MockAnchorElementPreconnector(
content::RenderFrameHost& render_frame_host) {}
~MockAnchorElementPreconnector() override = default;
void MaybePreconnect(const GURL& target) override { target_ = target; }
absl::optional<GURL>& Target() { return target_; }
private:
absl::optional<GURL> target_;
};
class TestPrefetchService : public PrefetchService {
public:
explicit TestPrefetchService(BrowserContext* browser_context)
: PrefetchService(browser_context) {}
void PrefetchUrl(
base::WeakPtr<PrefetchContainer> prefetch_container) override {
prefetches_.push_back(prefetch_container);
}
std::vector<base::WeakPtr<PrefetchContainer>> prefetches_;
};
class MockPrerenderer : public Prerenderer {
public:
~MockPrerenderer() override = default;
void ProcessCandidatesForPrerender(
const std::vector<blink::mojom::SpeculationCandidatePtr>& candidates)
override {
for (const auto& candidate : candidates) {
MaybePrerender(candidate);
}
}
bool MaybePrerender(
const blink::mojom::SpeculationCandidatePtr& candidate) override {
return prerenders_.insert(candidate->url).second;
}
bool ShouldWaitForPrerenderResult(const GURL& url) override {
return prerenders_.find(url) != prerenders_.end();
}
std::set<GURL> prerenders_;
};
class ScopedMockPrerenderer {
public:
explicit ScopedMockPrerenderer(PreloadingDecider* preloading_decider)
: preloading_decider_(preloading_decider) {
auto new_prerenderer = std::make_unique<MockPrerenderer>();
prerenderer_ = new_prerenderer.get();
old_prerenderer_ = preloading_decider_->SetPrerendererForTesting(
std::move(new_prerenderer));
}
~ScopedMockPrerenderer() {
preloading_decider_->SetPrerendererForTesting(std::move(old_prerenderer_));
}
MockPrerenderer* Get() { return prerenderer_.get(); }
private:
raw_ptr<PreloadingDecider> preloading_decider_;
raw_ptr<MockPrerenderer> prerenderer_;
std::unique_ptr<Prerenderer> old_prerenderer_;
};
class MockContentBrowserClient : public TestContentBrowserClient {
public:
MockContentBrowserClient() {
old_browser_client_ = SetBrowserClientForTesting(this);
}
~MockContentBrowserClient() override {
EXPECT_EQ(this, SetBrowserClientForTesting(old_browser_client_));
}
std::unique_ptr<AnchorElementPreconnectDelegate>
CreateAnchorElementPreconnectDelegate(
RenderFrameHost& render_frame_host) override {
auto delegate =
std::make_unique<MockAnchorElementPreconnector>(render_frame_host);
delegate_ = delegate.get();
return delegate;
}
MockAnchorElementPreconnector* GetDelegate() { return delegate_; }
private:
raw_ptr<ContentBrowserClient> old_browser_client_;
raw_ptr<MockAnchorElementPreconnector> delegate_;
};
class PreloadingDeciderTest : public RenderViewHostTestHarness {
public:
PreloadingDeciderTest() {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
content::features::kPrefetchUseContentRefactor,
{{"proxy_host", "https://testproxyhost.com"}});
}
void SetUp() override {
RenderViewHostTestHarness::SetUp();
browser_context_ = std::make_unique<TestBrowserContext>();
web_contents_ = TestWebContents::Create(
browser_context_.get(),
SiteInstanceImpl::Create(browser_context_.get()));
web_contents_->SetDelegate(&web_contents_delegate_);
web_contents_->NavigateAndCommit(GetSameOriginUrl("/"));
prefetch_service_ =
std::make_unique<TestPrefetchService>(GetBrowserContext());
PrefetchDocumentManager::SetPrefetchServiceForTesting(
prefetch_service_.get());
}
void TearDown() override {
web_contents_.reset();
browser_context_.reset();
PrefetchDocumentManager::SetPrefetchServiceForTesting(nullptr);
RenderViewHostTestHarness::TearDown();
}
RenderFrameHostImpl& GetPrimaryMainFrame() {
return web_contents_->GetPrimaryPage().GetMainDocument();
}
GURL GetSameOriginUrl(const std::string& path) {
return GURL("https://example.com" + path);
}
GURL GetCrossOriginUrl(const std::string& path) {
return GURL("https://other.example.com" + path);
}
TestPrefetchService* GetPrefetchService() { return prefetch_service_.get(); }
private:
test::ScopedPrerenderFeatureList prerender_feature_list_;
std::unique_ptr<TestBrowserContext> browser_context_;
std::unique_ptr<TestWebContents> web_contents_;
PrerenderWebContentsDelegate web_contents_delegate_;
std::unique_ptr<TestPrefetchService> prefetch_service_;
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(PreloadingDeciderTest, DefaultEagernessCandidatesStartOnStandby) {
auto* preloading_decider =
PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
ASSERT_TRUE(preloading_decider != nullptr);
// Create list of SpeculationCandidatePtrs.
std::vector<std::tuple<bool, GURL, blink::mojom::SpeculationAction,
blink::mojom::SpeculationEagerness>>
test_cases{{true, GetCrossOriginUrl("/candidate1.html"),
blink::mojom::SpeculationAction::kPrefetch,
blink::mojom::SpeculationEagerness::kDefault},
{false, GetCrossOriginUrl("/candidate2.html"),
blink::mojom::SpeculationAction::kPrefetch,
blink::mojom::SpeculationEagerness::kEager},
{true, GetCrossOriginUrl("/candidate1.html"),
blink::mojom::SpeculationAction::kPrerender,
blink::mojom::SpeculationEagerness::kDefault},
{false, GetCrossOriginUrl("/candidate2.html"),
blink::mojom::SpeculationAction::kPrerender,
blink::mojom::SpeculationEagerness::kEager}};
std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
for (const auto& [should_be_on_standby, url, action, eagerness] :
test_cases) {
auto candidate = blink::mojom::SpeculationCandidate::New();
candidate->action = action;
candidate->url = url;
candidate->referrer = blink::mojom::Referrer::New();
candidate->eagerness = eagerness;
candidates.push_back(std::move(candidate));
}
preloading_decider->UpdateSpeculationCandidates(candidates);
for (const auto& [should_be_on_standby, url, action, eagerness] :
test_cases) {
EXPECT_EQ(should_be_on_standby,
preloading_decider->IsOnStandByForTesting(url, action));
}
}
TEST_F(PreloadingDeciderTest, PrefetchOnPointerDownHeuristics) {
base::test::ScopedFeatureList scoped_features;
scoped_features.InitWithFeatures(
{blink::features::kSpeculationRulesPointerDownHeuristics}, {});
MockContentBrowserClient browser_client;
auto* preloading_decider =
PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
ASSERT_TRUE(preloading_decider != nullptr);
auto* preconnect_delegate = browser_client.GetDelegate();
EXPECT_TRUE(preconnect_delegate != nullptr);
// Create list of SpeculationCandidatePtrs.
std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
auto candidate1 = blink::mojom::SpeculationCandidate::New();
candidate1->action = blink::mojom::SpeculationAction::kPrefetch;
candidate1->requires_anonymous_client_ip_when_cross_origin = true;
candidate1->url = GetCrossOriginUrl("/candidate1.html");
candidate1->referrer = blink::mojom::Referrer::New();
candidate1->eagerness = blink::mojom::SpeculationEagerness::kDefault;
candidates.push_back(std::move(candidate1));
preloading_decider->UpdateSpeculationCandidates(candidates);
// It should not pass kDefault candidates directly
EXPECT_TRUE(GetPrefetchService()->prefetches_.empty());
preloading_decider->OnPointerDown(GetCrossOriginUrl("/candidate1.html"));
EXPECT_FALSE(
preconnect_delegate->Target().has_value()); // Shouldn't preconnect
EXPECT_EQ(
1u,
GetPrefetchService()->prefetches_.size()); // It should only prefetch
// Another pointer down should not change anything
preloading_decider->OnPointerDown(GetCrossOriginUrl("/candidate1.html"));
EXPECT_FALSE(preconnect_delegate->Target().has_value());
EXPECT_EQ(1u, GetPrefetchService()->prefetches_.size());
// It should preconnect if the target is not safe to prefetch
preloading_decider->OnPointerDown(GetCrossOriginUrl("/candidate2.html"));
EXPECT_TRUE(preconnect_delegate->Target().has_value());
EXPECT_EQ(1u, GetPrefetchService()->prefetches_.size());
}
TEST_F(PreloadingDeciderTest, PrerenderOnPointerDownHeuristics) {
base::test::ScopedFeatureList scoped_features;
scoped_features.InitWithFeatures(
{blink::features::kSpeculationRulesPointerDownHeuristics}, {});
MockContentBrowserClient browser_client;
auto* preloading_decider =
PreloadingDecider::GetOrCreateForCurrentDocument(&GetPrimaryMainFrame());
ASSERT_TRUE(preloading_decider != nullptr);
ScopedMockPrerenderer prerenderer(preloading_decider);
auto* preconnect_delegate = browser_client.GetDelegate();
EXPECT_TRUE(preconnect_delegate != nullptr);
// Create list of SpeculationCandidatePtrs.
std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
auto CreateCandidate = [this](const auto& action, const auto& url,
const auto& eagerness) {
auto candidate = blink::mojom::SpeculationCandidate::New();
candidate->action = action;
candidate->url = GetSameOriginUrl(url);
candidate->referrer = blink::mojom::Referrer::New();
candidate->eagerness = eagerness;
return candidate;
};
candidates.push_back(CreateCandidate(
blink::mojom::SpeculationAction::kPrerender, "/candidate1.html",
blink::mojom::SpeculationEagerness::kDefault));
candidates.push_back(CreateCandidate(
blink::mojom::SpeculationAction::kPrefetch, "/candidate2.html",
blink::mojom::SpeculationEagerness::kDefault));
preloading_decider->UpdateSpeculationCandidates(candidates);
// It should not pass kDefault candidates directly
EXPECT_TRUE(prerenderer.Get()->prerenders_.empty());
EXPECT_TRUE(GetPrefetchService()->prefetches_.empty());
preloading_decider->OnPointerDown(GetSameOriginUrl("/candidate1.html"));
EXPECT_FALSE(
preconnect_delegate->Target().has_value()); // Shouldn't preconnect.
EXPECT_EQ(0u,
GetPrefetchService()->prefetches_.size()); // Shouldn't prefetch.
EXPECT_EQ(1u,
prerenderer.Get()->prerenders_.size()); // Should prerender.
// Another pointer down should not change anything
preloading_decider->OnPointerDown(GetSameOriginUrl("/candidate1.html"));
EXPECT_FALSE(preconnect_delegate->Target().has_value());
EXPECT_EQ(0u, GetPrefetchService()->prefetches_.size());
EXPECT_EQ(1u, prerenderer.Get()->prerenders_.size());
// It should prefetch if the target is safe to prefetch.
preloading_decider->OnPointerDown(GetSameOriginUrl("/candidate2.html"));
EXPECT_FALSE(preconnect_delegate->Target().has_value());
EXPECT_EQ(1u, GetPrefetchService()->prefetches_.size());
EXPECT_EQ(1u, prerenderer.Get()->prerenders_.size());
// It should preconnect if the target is not safe to prerender nor safe to
// prefetch.
preloading_decider->OnPointerDown(GetSameOriginUrl("/candidate3.html"));
EXPECT_TRUE(preconnect_delegate->Target().has_value());
EXPECT_EQ(1u, GetPrefetchService()->prefetches_.size());
EXPECT_EQ(1u, prerenderer.Get()->prerenders_.size());
}
} // namespace
} // namespace content