blob: 2698ac58063c956fb069503d6f0682994bdd39e3 [file] [log] [blame]
// Copyright 2020 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/prerender/prerender_host.h"
#include <memory>
#include "base/functional/bind.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/preloading/preload_pipeline_info_impl.h"
#include "content/browser/preloading/preloading.h"
#include "content/browser/preloading/prerender/prerender_attributes.h"
#include "content/browser/preloading/prerender/prerender_features.h"
#include "content/browser/preloading/prerender/prerender_final_status.h"
#include "content/browser/preloading/prerender/prerender_host.h"
#include "content/browser/preloading/prerender/prerender_host_registry.h"
#include "content/browser/preloading/prerender/prerender_metrics.h"
#include "content/public/browser/preload_pipeline_info.h"
#include "content/public/browser/preloading.h"
#include "content/public/browser/preloading_data.h"
#include "content/public/test/mock_web_contents_observer.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/preloading_test_util.h"
#include "content/public/test/prerender_test_util.h"
#include "content/test/mock_commit_deferring_condition.h"
#include "content/test/navigation_simulator_impl.h"
#include "content/test/test_render_frame_host.h"
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
#include "net/http/http_request_headers.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/common/loader/loader_constants.h"
#include "third_party/blink/public/mojom/speculation_rules/speculation_rules.mojom-shared.h"
namespace content {
namespace {
using ::testing::_;
TEST(IsActivationHeaderMatchTest, OrderInsensitive) {
PrerenderCancellationReason reason = PrerenderCancellationReason(
PrerenderFinalStatus::kActivationNavigationParameterMismatch);
net::HttpRequestHeaders prerender_headers;
prerender_headers.AddHeadersFromString(
"name1: value1 \r\n name2: value2 \r\n name3: value3");
net::HttpRequestHeaders potential_activation_headers;
potential_activation_headers.AddHeadersFromString(
"name2: value2 \r\n name3:value3 \r\n name1: value1 ");
EXPECT_TRUE(PrerenderHost::IsActivationHeaderMatch(
potential_activation_headers, prerender_headers, reason));
}
TEST(IsActivationHeaderMatchTest, KeyCaseInsensitive) {
PrerenderCancellationReason reason = PrerenderCancellationReason(
PrerenderFinalStatus::kActivationNavigationParameterMismatch);
net::HttpRequestHeaders prerender_headers;
prerender_headers.AddHeadersFromString(
"NAME1: value1 \r\n name2: value2 \r\n name3: value3");
net::HttpRequestHeaders potential_activation_headers;
potential_activation_headers.AddHeadersFromString(
"name1: value1 \r\n name2: value2 \r\n name3: value3 ");
EXPECT_TRUE(PrerenderHost::IsActivationHeaderMatch(
potential_activation_headers, prerender_headers, reason));
}
TEST(IsActivationHeaderMatchTest, ValueCaseInsensitive) {
PrerenderCancellationReason reason = PrerenderCancellationReason(
PrerenderFinalStatus::kActivationNavigationParameterMismatch);
net::HttpRequestHeaders prerender_headers;
prerender_headers.AddHeadersFromString(
"name1: value1 \r\n name2: value2 \r\n name3: value3");
net::HttpRequestHeaders potential_activation_headers;
potential_activation_headers.AddHeadersFromString(
"name1: value1 \r\n name2: VALUE2 \r\n name3: value3 ");
EXPECT_TRUE(PrerenderHost::IsActivationHeaderMatch(
potential_activation_headers, prerender_headers, reason));
}
TEST(IsActivationHeaderMatchTest, CalculateMismatchedHeaders) {
auto same_key_value = [](const PrerenderMismatchedHeaders& a,
const PrerenderMismatchedHeaders& b) {
return a.header_name == b.header_name &&
a.initial_value == b.initial_value &&
a.activation_value == b.activation_value;
};
{
PrerenderCancellationReason reason = PrerenderCancellationReason(
PrerenderFinalStatus::kActivationNavigationParameterMismatch);
net::HttpRequestHeaders prerender_headers;
prerender_headers.AddHeadersFromString(
"name1: value1 \r\n name2: value2 \r\n name3: value3");
net::HttpRequestHeaders potential_headers;
potential_headers.AddHeadersFromString(
"name1: value1 \r\n name2: value2 \r\n name3: value3");
EXPECT_TRUE(PrerenderHost::IsActivationHeaderMatch(
potential_headers, prerender_headers, reason));
EXPECT_FALSE(reason.GetPrerenderMismatchedHeaders());
}
{
PrerenderCancellationReason reason = PrerenderCancellationReason(
PrerenderFinalStatus::kActivationNavigationParameterMismatch);
net::HttpRequestHeaders prerender_headers;
prerender_headers.AddHeadersFromString("");
net::HttpRequestHeaders potential_headers;
potential_headers.AddHeadersFromString("");
EXPECT_TRUE(PrerenderHost::IsActivationHeaderMatch(
potential_headers, prerender_headers, reason));
EXPECT_FALSE(reason.GetPrerenderMismatchedHeaders());
}
{
PrerenderCancellationReason reason = PrerenderCancellationReason(
PrerenderFinalStatus::kActivationNavigationParameterMismatch);
net::HttpRequestHeaders prerender_headers;
prerender_headers.AddHeadersFromString(
"name1: value1 \r\n name2: value2 \r\n name3: value3 \r\n name5: "
"value3");
net::HttpRequestHeaders potential_headers;
potential_headers.AddHeadersFromString(
"name1: value1 \r\n name3: value2 \r\n name4: value4 \r\n name5: "
"value3");
EXPECT_FALSE(PrerenderHost::IsActivationHeaderMatch(
potential_headers, prerender_headers, reason));
std::vector<PrerenderMismatchedHeaders> mismatched_headers_expected;
mismatched_headers_expected.emplace_back("name2", "value2", std::nullopt);
mismatched_headers_expected.emplace_back("name3", "value3", "value2");
mismatched_headers_expected.emplace_back("name4", std::nullopt, "value4");
EXPECT_TRUE(std::equal(reason.GetPrerenderMismatchedHeaders()->begin(),
reason.GetPrerenderMismatchedHeaders()->end(),
mismatched_headers_expected.begin(),
mismatched_headers_expected.end(), same_key_value));
}
{
PrerenderCancellationReason reason = PrerenderCancellationReason(
PrerenderFinalStatus::kActivationNavigationParameterMismatch);
net::HttpRequestHeaders prerender_headers;
prerender_headers.AddHeadersFromString(
"name5: value1 \r\n name6: value2 \r\n name7: value3");
net::HttpRequestHeaders potential_headers;
potential_headers.AddHeadersFromString("name2: value1");
EXPECT_FALSE(PrerenderHost::IsActivationHeaderMatch(
potential_headers, prerender_headers, reason));
std::vector<PrerenderMismatchedHeaders> mismatched_headers_expected;
mismatched_headers_expected.emplace_back("name2", std::nullopt, "value1");
mismatched_headers_expected.emplace_back("name5", "value1", std::nullopt);
mismatched_headers_expected.emplace_back("name6", "value2", std::nullopt);
mismatched_headers_expected.emplace_back("name7", "value3", std::nullopt);
EXPECT_TRUE(std::equal(reason.GetPrerenderMismatchedHeaders()->begin(),
reason.GetPrerenderMismatchedHeaders()->end(),
mismatched_headers_expected.begin(),
mismatched_headers_expected.end(), same_key_value));
}
{
PrerenderCancellationReason reason = PrerenderCancellationReason(
PrerenderFinalStatus::kActivationNavigationParameterMismatch);
net::HttpRequestHeaders prerender_headers;
prerender_headers.AddHeadersFromString("name5: value1 \r\n name6: value2");
net::HttpRequestHeaders potential_headers;
potential_headers.AddHeadersFromString(
"name2: value1 \r\n name6: value2 \r\n name7: value3 \r\n name8: "
"value3");
EXPECT_FALSE(PrerenderHost::IsActivationHeaderMatch(
potential_headers, prerender_headers, reason));
std::vector<PrerenderMismatchedHeaders> mismatched_headers_expected;
mismatched_headers_expected.emplace_back("name2", std::nullopt, "value1");
mismatched_headers_expected.emplace_back("name5", "value1", std::nullopt);
mismatched_headers_expected.emplace_back("name7", std::nullopt, "value3");
mismatched_headers_expected.emplace_back("name8", std::nullopt, "value3");
EXPECT_TRUE(std::equal(reason.GetPrerenderMismatchedHeaders()->begin(),
reason.GetPrerenderMismatchedHeaders()->end(),
mismatched_headers_expected.begin(),
mismatched_headers_expected.end(), same_key_value));
}
{
PrerenderCancellationReason reason = PrerenderCancellationReason(
PrerenderFinalStatus::kActivationNavigationParameterMismatch);
net::HttpRequestHeaders prerender_headers;
prerender_headers.AddHeadersFromString("");
net::HttpRequestHeaders potential_headers;
potential_headers.AddHeadersFromString(
"name1: value1 \r\n name2: value2 \r\n name3: value3");
EXPECT_FALSE(PrerenderHost::IsActivationHeaderMatch(
potential_headers, prerender_headers, reason));
std::vector<PrerenderMismatchedHeaders> mismatched_headers_expected;
mismatched_headers_expected.emplace_back("name1", std::nullopt, "value1");
mismatched_headers_expected.emplace_back("name2", std::nullopt, "value2");
mismatched_headers_expected.emplace_back("name3", std::nullopt, "value3");
EXPECT_TRUE(std::equal(reason.GetPrerenderMismatchedHeaders()->begin(),
reason.GetPrerenderMismatchedHeaders()->end(),
mismatched_headers_expected.begin(),
mismatched_headers_expected.end(), same_key_value));
}
{
PrerenderCancellationReason reason = PrerenderCancellationReason(
PrerenderFinalStatus::kActivationNavigationParameterMismatch);
net::HttpRequestHeaders prerender_headers;
prerender_headers.AddHeadersFromString(
"name1: value1 \r\n name2: value2 \r\n name3: value3");
net::HttpRequestHeaders potential_headers;
potential_headers.AddHeadersFromString("");
EXPECT_FALSE(PrerenderHost::IsActivationHeaderMatch(
potential_headers, prerender_headers, reason));
std::vector<PrerenderMismatchedHeaders> mismatched_headers_expected;
mismatched_headers_expected.emplace_back("name1", "value1", std::nullopt);
mismatched_headers_expected.emplace_back("name2", "value2", std::nullopt);
mismatched_headers_expected.emplace_back("name3", "value3", std::nullopt);
EXPECT_TRUE(std::equal(reason.GetPrerenderMismatchedHeaders()->begin(),
reason.GetPrerenderMismatchedHeaders()->end(),
mismatched_headers_expected.begin(),
mismatched_headers_expected.end(), same_key_value));
}
}
using ExpectedReadyForActivationState =
base::StrongAlias<class ExpectedReadyForActivationStateType, bool>;
// Finish a prerendering navigation that was already started with
// CreateAndStartHost().
void CommitPrerenderNavigation(
PrerenderHost& host,
ExpectedReadyForActivationState ready_for_activation =
ExpectedReadyForActivationState(true),
scoped_refptr<net::HttpResponseHeaders> headers = nullptr) {
// Normally we could use EmbeddedTestServer to provide a response, but these
// tests use RenderViewHostImplTestHarness so the load goes through a
// TestNavigationURLLoader which we don't have access to in order to
// complete. Use NavigationSimulator to finish the navigation.
FrameTreeNode* ftn = FrameTreeNode::From(host.GetPrerenderedMainFrameHost());
std::unique_ptr<NavigationSimulator> sim =
NavigationSimulatorImpl::CreateFromPendingInFrame(ftn);
sim->SetResponseHeaders(headers);
sim->Commit();
EXPECT_EQ(host.is_ready_for_activation(), ready_for_activation.value());
}
std::unique_ptr<NavigationSimulatorImpl> CreateActivation(
const GURL& prerendering_url,
WebContentsImpl& web_contents) {
std::unique_ptr<NavigationSimulatorImpl> navigation =
NavigationSimulatorImpl::CreateRendererInitiated(
prerendering_url, web_contents.GetPrimaryMainFrame());
navigation->SetReferrer(blink::mojom::Referrer::New(
web_contents.GetPrimaryMainFrame()->GetLastCommittedURL(),
network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin));
return navigation;
}
class PrerenderHostTest : public RenderViewHostImplTestHarness {
public:
PrerenderHostTest() {
scoped_feature_list_.InitAndEnableFeature(
blink::features::kPrerender2MainFrameNavigation);
}
~PrerenderHostTest() override = default;
void SetUp() override {
RenderViewHostImplTestHarness::SetUp();
web_contents_delegate_ =
std::make_unique<test::ScopedPrerenderWebContentsDelegate>(*contents());
contents()->NavigateAndCommit(GURL("https://example.com"));
}
PrerenderAttributes GeneratePrerenderAttributes(const GURL& url) {
return GeneratePrerenderAttributesWithPredicate(url,
/*url_match_predicate=*/{});
}
PrerenderAttributes GeneratePrerenderAttributesWithPredicate(
const GURL& url,
base::RepeatingCallback<bool(const GURL&,
const std::optional<content::UrlMatchType>&)>
url_match_predicate) {
RenderFrameHostImpl* rfh = contents()->GetPrimaryMainFrame();
return PrerenderAttributes(
url, PreloadingTriggerType::kSpeculationRule,
/*embedder_histogram_suffix=*/"", SpeculationRulesParams(), Referrer(),
/*no_vary_search_hint=*/std::nullopt, rfh, contents()->GetWeakPtr(),
ui::PAGE_TRANSITION_LINK,
/*should_warm_up_compositor=*/false,
/*should_prepare_paint_tree=*/false, std::move(url_match_predicate),
/*prerender_navigation_handle_callback=*/{},
PreloadPipelineInfoImpl::Create(
/*planned_max_preloading_type=*/PreloadingType::kPrerender));
}
void ExpectFinalStatus(PrerenderFinalStatus status) {
// Check FinalStatus in UMA.
histogram_tester_.ExpectUniqueSample(
"Prerender.Experimental.PrerenderHostFinalStatus.SpeculationRule",
status, 1);
// Check all entries in UKM to make sure that the recorded FinalStatus is
// equal to `status`. At least one entry should exist.
bool final_status_entry_found = false;
const auto entries = ukm_recorder_.GetEntriesByName(
ukm::builders::PrerenderPageLoad::kEntryName);
for (const ukm::mojom::UkmEntry* entry : entries) {
if (ukm_recorder_.EntryHasMetric(
entry, ukm::builders::PrerenderPageLoad::kFinalStatusName)) {
final_status_entry_found = true;
ukm_recorder_.ExpectEntryMetric(
entry, ukm::builders::PrerenderPageLoad::kFinalStatusName,
static_cast<int>(status));
}
}
EXPECT_TRUE(final_status_entry_found);
}
PrerenderHostRegistry& registry() {
return *contents()->GetPrerenderHostRegistry();
}
private:
test::ScopedPrerenderFeatureList prerender_feature_list_;
std::unique_ptr<test::ScopedPrerenderWebContentsDelegate>
web_contents_delegate_;
base::HistogramTester histogram_tester_;
ukm::TestAutoSetUkmRecorder ukm_recorder_;
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(PrerenderHostTest, IsNoVarySearchHeaderSet) {
// Start prerendering a page.
const GURL kPrerenderingUrl("https://example.com/next");
FrameTreeNodeId prerender_frame_tree_node_id =
contents()->AddPrerender(kPrerenderingUrl);
PrerenderHost* prerender_host =
registry().FindNonReservedHostById(prerender_frame_tree_node_id);
CommitPrerenderNavigation(
*prerender_host, ExpectedReadyForActivationState(true),
net::HttpResponseHeaders::Builder(net::HttpVersion(1, 1), "200 OK")
.AddHeader("No-Vary-Search", "params=(\"a\")")
.Build());
EXPECT_TRUE(prerender_host->no_vary_search().has_value());
}
TEST_F(PrerenderHostTest, Activate) {
// Start prerendering a page.
const GURL kPrerenderingUrl("https://example.com/next");
FrameTreeNodeId prerender_frame_tree_node_id =
contents()->AddPrerender(kPrerenderingUrl);
PrerenderHost* prerender_host =
registry().FindNonReservedHostById(prerender_frame_tree_node_id);
CommitPrerenderNavigation(*prerender_host);
// Perform a navigation in the primary frame tree which activates the
// prerendered page.
contents()->ActivatePrerenderedPage(kPrerenderingUrl);
ExpectFinalStatus(PrerenderFinalStatus::kActivated);
}
TEST_F(PrerenderHostTest, DontActivate) {
// Start the prerendering navigation, but don't activate it.
const GURL kPrerenderingUrl("https://example.com/next");
const FrameTreeNodeId prerender_frame_tree_node_id =
contents()->AddPrerender(kPrerenderingUrl);
registry().CancelHost(prerender_frame_tree_node_id,
PrerenderFinalStatus::kDestroyed);
ExpectFinalStatus(PrerenderFinalStatus::kDestroyed);
}
// Tests that cross-site main frame navigations in a prerendered page cannot
// occur even if they start after the prerendered page has been reserved for
// activation.
TEST_F(PrerenderHostTest, MainFrameNavigationForReservedHost) {
// Start prerendering a page.
const GURL kPrerenderingUrl("https://example.com/next");
RenderFrameHostImpl* prerender_rfh =
contents()->AddPrerenderAndCommitNavigation(kPrerenderingUrl);
FrameTreeNode* ftn = prerender_rfh->frame_tree_node();
EXPECT_FALSE(ftn->HasNavigation());
test::PrerenderHostObserver prerender_host_observer(*contents(),
kPrerenderingUrl);
// Now navigate the primary page to the prerendered URL so that we activate
// the prerender. Use a CommitDeferringCondition to pause activation
// before it completes.
std::unique_ptr<NavigationSimulatorImpl> navigation;
{
MockCommitDeferringConditionInstaller installer(
kPrerenderingUrl, CommitDeferringCondition::Result::kDefer);
// Start trying to activate the prerendered page.
navigation = CreateActivation(kPrerenderingUrl, *contents());
navigation->Start();
// Wait for the condition to pause the activation.
installer.WaitUntilInstalled();
installer.condition().WaitUntilInvoked();
// The request should be deferred by the condition.
NavigationRequest* navigation_request =
static_cast<NavigationRequest*>(navigation->GetNavigationHandle());
EXPECT_TRUE(
navigation_request->IsCommitDeferringConditionDeferredForTesting());
// The primary page should still be the original page.
EXPECT_EQ(contents()->GetLastCommittedURL(), "https://example.com/");
const GURL kBadUrl("https://example2.test/");
TestNavigationManager tno(contents(), kBadUrl);
// Start a cross-site navigation in the prerendered page. It should be
// cancelled.
auto navigation_2 = NavigationSimulatorImpl::CreateRendererInitiated(
kBadUrl, prerender_rfh);
navigation_2->Start();
EXPECT_EQ(NavigationThrottle::CANCEL,
navigation_2->GetLastThrottleCheckResult());
ASSERT_TRUE(tno.WaitForNavigationFinished());
EXPECT_FALSE(tno.was_committed());
// The cross-site navigation cancels the activation.
installer.condition().CallResumeClosure();
prerender_host_observer.WaitForDestroyed();
EXPECT_FALSE(prerender_host_observer.was_activated());
EXPECT_EQ(registry().FindHostByUrlForTesting(kPrerenderingUrl), nullptr);
ExpectFinalStatus(
PrerenderFinalStatus::kCrossSiteNavigationInMainFrameNavigation);
}
// The activation falls back to regular navigation.
navigation->Commit();
EXPECT_EQ(contents()->GetPrimaryMainFrame()->GetLastCommittedURL(),
kPrerenderingUrl);
}
// Tests that an activation can successfully commit after the prerendering page
// has updated its PageState.
TEST_F(PrerenderHostTest, ActivationAfterPageStateUpdate) {
// Start prerendering a page.
const GURL kPrerenderingUrl("https://example.com/next");
const FrameTreeNodeId prerender_frame_tree_node_id =
registry().CreateAndStartHost(
GeneratePrerenderAttributes(kPrerenderingUrl));
PrerenderHost* prerender_host =
registry().FindNonReservedHostById(prerender_frame_tree_node_id);
CommitPrerenderNavigation(*prerender_host);
auto* prerender_root_ftn =
FrameTreeNode::GloballyFindByID(prerender_frame_tree_node_id);
RenderFrameHostImpl* prerender_rfh = prerender_root_ftn->current_frame_host();
NavigationEntryImpl* prerender_nav_entry =
prerender_root_ftn->frame_tree().controller().GetLastCommittedEntry();
FrameNavigationEntry* prerender_root_fne =
prerender_nav_entry->GetFrameEntry(prerender_root_ftn);
auto page_state = blink::PageState::CreateForTestingWithSequenceNumbers(
GURL("about:blank"), prerender_root_fne->item_sequence_number(),
prerender_root_fne->document_sequence_number());
// Update PageState for prerender RFH, causing it to become different from
// the one stored in RFH's last commit params.
static_cast<mojom::FrameHost*>(prerender_rfh)->UpdateState(page_state);
// Perform a navigation in the primary frame tree which activates the
// prerendered page. The main expectation is that this navigation commits
// successfully and doesn't hit any CHECKs.
contents()->ActivatePrerenderedPage(kPrerenderingUrl);
ExpectFinalStatus(PrerenderFinalStatus::kActivated);
// Ensure that the the page_state was preserved.
EXPECT_EQ(contents()->GetPrimaryMainFrame(), prerender_rfh);
NavigationEntryImpl* activated_nav_entry =
contents()->GetController().GetLastCommittedEntry();
EXPECT_EQ(page_state,
activated_nav_entry
->GetFrameEntry(contents()->GetPrimaryFrameTree().root())
->page_state());
}
// Test that WebContentsObserver::LoadProgressChanged is not invoked when the
// page gets loaded while prerendering but is invoked on prerender activation.
// Check that in case the load is incomplete with load progress
// `kPartialLoadProgress`, we would see
// LoadProgressChanged(kPartialLoadProgress) called on activation.
TEST_F(PrerenderHostTest, LoadProgressChangedInvokedOnActivation) {
contents()->set_minimum_delay_between_loading_updates_for_testing(
base::Milliseconds(0));
// Initialize a MockWebContentsObserver and ensure that LoadProgressChanged is
// not invoked while prerendering.
testing::NiceMock<MockWebContentsObserver> observer(contents());
testing::InSequence s;
EXPECT_CALL(observer, LoadProgressChanged(testing::_)).Times(0);
// Start prerendering a page and commit prerender navigation.
const GURL kPrerenderingUrl("https://example.com/next");
constexpr double kPartialLoadProgress = 0.7;
RenderFrameHostImpl* prerender_rfh =
contents()->AddPrerenderAndCommitNavigation(kPrerenderingUrl);
FrameTreeNode* ftn = prerender_rfh->frame_tree_node();
EXPECT_FALSE(ftn->HasNavigation());
// Verify and clear all expectations on the mock observer before setting new
// ones.
testing::Mock::VerifyAndClearExpectations(&observer);
// Activate the prerendered page. This should result in invoking
// LoadProgressChanged for the following cases:
{
// 1) During DidStartLoading LoadProgressChanged is invoked with
// kInitialLoadProgress value.
EXPECT_CALL(observer, LoadProgressChanged(blink::kInitialLoadProgress));
// Verify that DidFinishNavigation is invoked before final load progress
// notification.
EXPECT_CALL(observer, DidFinishNavigation(testing::_));
// 2) After DidCommitNavigationInternal on activation with
// LoadProgressChanged is invoked with kPartialLoadProgress value.
EXPECT_CALL(observer, LoadProgressChanged(kPartialLoadProgress));
// 3) During DidStopLoading LoadProgressChanged is invoked with
// kFinalLoadProgress.
EXPECT_CALL(observer, LoadProgressChanged(blink::kFinalLoadProgress));
}
// Set load_progress value to kPartialLoadProgress in prerendering state,
// this should result in invoking LoadProgressChanged(kPartialLoadProgress) on
// activation.
prerender_rfh->GetPage().set_load_progress(kPartialLoadProgress);
// Perform a navigation in the primary frame tree which activates the
// prerendered page.
contents()->ActivatePrerenderedPage(kPrerenderingUrl);
ExpectFinalStatus(PrerenderFinalStatus::kActivated);
}
TEST_F(PrerenderHostTest, DontCancelPrerenderWhenTriggerGetsHidden) {
const GURL kPrerenderingUrl = GURL("https://example.com/empty.html");
const FrameTreeNodeId prerender_frame_tree_node_id =
registry().CreateAndStartHost(
GeneratePrerenderAttributes(kPrerenderingUrl));
PrerenderHost* prerender_host =
registry().FindNonReservedHostById(prerender_frame_tree_node_id);
ASSERT_NE(prerender_host, nullptr);
CommitPrerenderNavigation(*prerender_host);
// Changing the visibility state to HIDDEN will not stop prerendering.
contents()->WasHidden();
// Activation from the foreground page should succeed.
contents()->WasShown();
contents()->ActivatePrerenderedPage(kPrerenderingUrl);
ExpectFinalStatus(PrerenderFinalStatus::kActivated);
}
TEST_F(PrerenderHostTest, CancelActivationFromHiddenPage) {
const GURL kPrerenderingUrl = GURL("https://example.com/empty.html");
const FrameTreeNodeId prerender_frame_tree_node_id =
registry().CreateAndStartHost(
GeneratePrerenderAttributes(kPrerenderingUrl));
PrerenderHost* prerender_host =
registry().FindNonReservedHostById(prerender_frame_tree_node_id);
ASSERT_NE(prerender_host, nullptr);
CommitPrerenderNavigation(*prerender_host);
// Changing the visibility state to HIDDEN will not stop prerendering.
contents()->WasHidden();
// Activation from the background page should fail.
test::PrerenderHostObserver prerender_host_observer(
*contents(), prerender_frame_tree_node_id);
std::unique_ptr<NavigationSimulatorImpl> navigation =
NavigationSimulatorImpl::CreateRendererInitiated(
kPrerenderingUrl, contents()->GetPrimaryMainFrame());
navigation->SetReferrer(blink::mojom::Referrer::New(
contents()->GetPrimaryMainFrame()->GetLastCommittedURL(),
network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin));
navigation->Commit();
prerender_host_observer.WaitForDestroyed();
EXPECT_FALSE(prerender_host_observer.was_activated());
ExpectFinalStatus(PrerenderFinalStatus::kActivatedInBackground);
}
TEST_F(PrerenderHostTest, DontCancelPrerenderWhenTriggerGetsVisible) {
const GURL kPrerenderingUrl = GURL("https://example.com/empty.html");
const FrameTreeNodeId prerender_frame_tree_node_id =
registry().CreateAndStartHost(
GeneratePrerenderAttributes(kPrerenderingUrl));
PrerenderHost* prerender_host =
registry().FindNonReservedHostById(prerender_frame_tree_node_id);
ASSERT_NE(prerender_host, nullptr);
CommitPrerenderNavigation(*prerender_host);
// Changing the visibility state to VISIBLE will not stop prerendering.
contents()->WasShown();
contents()->ActivatePrerenderedPage(kPrerenderingUrl);
ExpectFinalStatus(PrerenderFinalStatus::kActivated);
}
// Skip this test on Android as it doesn't support the OCCLUDED state.
#if !BUILDFLAG(IS_ANDROID)
TEST_F(PrerenderHostTest, DontCancelPrerenderWhenTriggerGetsOcculded) {
const GURL kPrerenderingUrl = GURL("https://example.com/empty.html");
const FrameTreeNodeId prerender_frame_tree_node_id =
registry().CreateAndStartHost(
GeneratePrerenderAttributes(kPrerenderingUrl));
PrerenderHost* prerender_host =
registry().FindNonReservedHostById(prerender_frame_tree_node_id);
ASSERT_NE(prerender_host, nullptr);
CommitPrerenderNavigation(*prerender_host);
// Changing the visibility state to OCCLUDED will not stop prerendering.
contents()->WasOccluded();
contents()->ActivatePrerenderedPage(kPrerenderingUrl);
ExpectFinalStatus(PrerenderFinalStatus::kActivated);
}
#endif
TEST_F(PrerenderHostTest, UrlMatchPredicate) {
const GURL kPrerenderingUrl = GURL("https://example.com/empty.html");
base::RepeatingCallback callback = base::BindRepeating(
[](const GURL&, const std::optional<content::UrlMatchType>&) {
return true;
});
const FrameTreeNodeId prerender_frame_tree_node_id =
registry().CreateAndStartHost(
GeneratePrerenderAttributesWithPredicate(kPrerenderingUrl, callback));
PrerenderHost* prerender_host =
registry().FindNonReservedHostById(prerender_frame_tree_node_id);
ASSERT_NE(prerender_host, nullptr);
const GURL kActivatedUrl = GURL("https://example.com/empty.html?activate");
ASSERT_NE(kActivatedUrl, kPrerenderingUrl);
EXPECT_TRUE(prerender_host->IsUrlMatch(kActivatedUrl));
// Even if the predicate always returns true, a cross-origin url shouldn't be
// able to activate a prerendered page.
EXPECT_FALSE(
prerender_host->IsUrlMatch(GURL("https://example2.com/empty.html")));
}
// Regression test for https://crbug.com/1366046: This test will crash if
// PrerenderHost is set to "ready_for_activation" after getting canceled.
TEST_F(PrerenderHostTest, CanceledPrerenderCannotBeReadyForActivation) {
const GURL kPrerenderingUrl = GURL("https://example.com/empty.html");
auto* preloading_data = PreloadingData::GetOrCreateForWebContents(contents());
// Create new PreloadingAttempt and pass all the values corresponding to
// this prerendering attempt.
PreloadingURLMatchCallback same_url_matcher =
PreloadingData::GetSameURLMatcher(kPrerenderingUrl);
PreloadingAttempt* preloading_attempt = preloading_data->AddPreloadingAttempt(
content_preloading_predictor::kSpeculationRules,
PreloadingType::kPrerender, std::move(same_url_matcher),
contents()->GetPrimaryMainFrame()->GetPageUkmSourceId());
const FrameTreeNodeId prerender_frame_tree_node_id =
registry().CreateAndStartHost(
GeneratePrerenderAttributes(kPrerenderingUrl), preloading_attempt);
PrerenderHost* prerender_host =
registry().FindNonReservedHostById(prerender_frame_tree_node_id);
ASSERT_NE(prerender_host, nullptr);
// Registry keeps alive through this test, so it is safe to capture the
// reference to `registry`.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(base::BindLambdaForTesting([&]() {
registry().CancelHost(prerender_frame_tree_node_id,
PrerenderFinalStatus::kTriggerDestroyed);
})));
// For some reasons triggers want to set the failure reason by themselves,
// this would happen together with cancelling prerender.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&PreloadingAttempt::SetFailureReason,
base::Unretained(preloading_attempt),
static_cast<PreloadingFailureReason>(
static_cast<int>(PrerenderFinalStatus::kTriggerDestroyed) +
static_cast<int>(PreloadingFailureReason::
kPreloadingFailureReasonCommonEnd))));
base::RunLoop run_loop;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindLambdaForTesting([&]() {
CommitPrerenderNavigation(*prerender_host,
ExpectedReadyForActivationState(false));
run_loop.Quit();
}));
// Wait for the completion of CommitPrerenderNavigation() above.
run_loop.Run();
EXPECT_EQ(test::PreloadingAttemptAccessor(preloading_attempt)
.GetTriggeringOutcome(),
PreloadingTriggeringOutcome::kFailure);
}
TEST(AreHttpRequestHeadersCompatible, IgnoreRTT) {
PrerenderCancellationReason reason = PrerenderCancellationReason(
PrerenderFinalStatus::kActivationNavigationParameterMismatch);
const std::string prerender_headers = "rtt: 1 \r\n downlink: 3";
const std::string potential_activation_headers = "rtt: 2 \r\n downlink: 4";
EXPECT_TRUE(PrerenderHost::AreHttpRequestHeadersCompatible(
potential_activation_headers,
#if BUILDFLAG(IS_ANDROID)
/*potential_activation_additional_headers=*/"",
#endif // BUILDFLAG(IS_ANDROID)
prerender_headers, PreloadingTriggerType::kSpeculationRule,
/*embedder_histogram_suffix=*/"", /*allow_x_header_mismatch=*/false,
reason));
}
TEST(AreHttpRequestHeadersCompatible, XHeaders) {
PrerenderCancellationReason reason = PrerenderCancellationReason(
PrerenderFinalStatus::kActivationNavigationParameterMismatch);
const std::string prerender_headers = "x-hello: 1";
const std::string potential_activation_headers = "X-world: 2";
EXPECT_FALSE(PrerenderHost::AreHttpRequestHeadersCompatible(
potential_activation_headers,
#if BUILDFLAG(IS_ANDROID)
/*potential_activation_additional_headers=*/"",
#endif // BUILDFLAG(IS_ANDROID)
prerender_headers, PreloadingTriggerType::kSpeculationRule,
/*embedder_histogram_suffix=*/"", /*allow_x_header_mismatch=*/false,
reason));
EXPECT_TRUE(PrerenderHost::AreHttpRequestHeadersCompatible(
potential_activation_headers,
#if BUILDFLAG(IS_ANDROID)
/*potential_activation_additional_headers=*/"",
#endif // BUILDFLAG(IS_ANDROID)
prerender_headers, PreloadingTriggerType::kSpeculationRule,
/*embedder_histogram_suffix=*/"", /*allow_x_header_mismatch=*/true,
reason));
}
} // namespace
} // namespace content