910e62b5创建于 1月15日历史提交
// 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 "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_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.SetHeader("name1", "value1");
  prerender_headers.SetHeader("name2", "value2");
  prerender_headers.SetHeader("name3", "value3");
  net::HttpRequestHeaders potential_activation_headers;
  potential_activation_headers.SetHeader("name2", "value2");
  potential_activation_headers.SetHeader("name3", "value3");
  potential_activation_headers.SetHeader("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.SetHeader("NAME1", "value1");
  prerender_headers.SetHeader("name2", "value2");
  prerender_headers.SetHeader("name3", "value3");
  net::HttpRequestHeaders potential_activation_headers;
  potential_activation_headers.SetHeader("name1", "value1");
  potential_activation_headers.SetHeader("name2", "value2");
  potential_activation_headers.SetHeader("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.SetHeader("name1", "value1");
  prerender_headers.SetHeader("name2", "value2");
  prerender_headers.SetHeader("name3", "value3");
  net::HttpRequestHeaders potential_activation_headers;
  potential_activation_headers.SetHeader("name1", "value1");
  potential_activation_headers.SetHeader("name2", "VALUE2");
  potential_activation_headers.SetHeader("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.SetHeader("name1", "value1");
    prerender_headers.SetHeader("name2", "value2");
    prerender_headers.SetHeader("name3", "value3");
    net::HttpRequestHeaders potential_headers;
    potential_headers.SetHeader("name1", "value1");
    potential_headers.SetHeader("name2", "value2");
    potential_headers.SetHeader("name3", "value3");
    EXPECT_TRUE(PrerenderHost::IsActivationHeaderMatch(
        potential_headers, prerender_headers, reason));
    EXPECT_FALSE(reason.GetPrerenderMismatchedHeaders());
  }
  {
    PrerenderCancellationReason reason = PrerenderCancellationReason(
        PrerenderFinalStatus::kActivationNavigationParameterMismatch);
    net::HttpRequestHeaders prerender_headers;
    net::HttpRequestHeaders potential_headers;
    EXPECT_TRUE(PrerenderHost::IsActivationHeaderMatch(
        potential_headers, prerender_headers, reason));
    EXPECT_FALSE(reason.GetPrerenderMismatchedHeaders());
  }
  {
    PrerenderCancellationReason reason = PrerenderCancellationReason(
        PrerenderFinalStatus::kActivationNavigationParameterMismatch);
    net::HttpRequestHeaders prerender_headers;
    prerender_headers.SetHeader("name1", "value1");
    prerender_headers.SetHeader("name2", "value2");
    prerender_headers.SetHeader("name3", "value3");
    prerender_headers.SetHeader("name5", "value3");
    net::HttpRequestHeaders potential_headers;
    potential_headers.SetHeader("name1", "value1");
    potential_headers.SetHeader("name3", "value2");
    potential_headers.SetHeader("name4", "value4");
    potential_headers.SetHeader("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.SetHeader("name5", "value1");
    prerender_headers.SetHeader("name6", "value2");
    prerender_headers.SetHeader("name7", "value3");
    net::HttpRequestHeaders potential_headers;
    potential_headers.SetHeader("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.SetHeader("name5", "value1");
    prerender_headers.SetHeader("name6", "value2");
    net::HttpRequestHeaders potential_headers;
    potential_headers.SetHeader("name2", "value1");
    potential_headers.SetHeader("name6", "value2");
    potential_headers.SetHeader("name7", "value3");
    potential_headers.SetHeader("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;
    net::HttpRequestHeaders potential_headers;
    potential_headers.SetHeader("name1", "value1");
    potential_headers.SetHeader("name2", "value2");
    potential_headers.SetHeader("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.SetHeader("name1", "value1");
    prerender_headers.SetHeader("name2", "value2");
    prerender_headers.SetHeader("name3", "value3");
    net::HttpRequestHeaders potential_headers;
    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() 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,
        blink::mojom::SpeculationAction::kPrerender,
        std::move(url_match_predicate),
        /*prerender_navigation_handle_callback=*/{},
        PreloadPipelineInfoImpl::Create(
            /*planned_max_preloading_type=*/PreloadingType::kPrerender),
        /*allow_reuse=*/false,
        /*form_submission=*/false);
  }

  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_;
};

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