910e62b5创建于 1月15日历史提交
// 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/prerenderer_impl.h"

#include <array>

#include "base/strings/string_number_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/preloading/preloading_confidence.h"
#include "content/browser/preloading/prerender/prerender_features.h"
#include "content/browser/preloading/prerender/prerender_host_registry.h"
#include "content/public/browser/web_contents_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 PrerendererTest : public RenderViewHostTestHarness {
 public:
  PrerendererTest() = default;

  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_delegate_ =
        std::make_unique<test::ScopedPrerenderWebContentsDelegate>(
            *web_contents_);
    web_contents_->NavigateAndCommit(GURL("https://example.com"));
  }

  void TearDown() override {
    web_contents_.reset();
    browser_context_.reset();
    RenderViewHostTestHarness::TearDown();
  }

  RenderFrameHostImpl* GetRenderFrameHost() {
    return web_contents_->GetPrimaryMainFrame();
  }

  GURL GetSameOriginUrl(const std::string& path) {
    return GURL("https://example.com" + path);
  }

  GURL GetCrossSiteUrl(const std::string& path) {
    return GURL("https://example2.com" + path);
  }

  PrerenderHostRegistry* GetPrerenderHostRegistry() const {
    return web_contents_->GetPrerenderHostRegistry();
  }

  blink::mojom::SpeculationCandidatePtr CreatePrerenderCandidate(
      const GURL& url) {
    auto candidate = blink::mojom::SpeculationCandidate::New();
    candidate->action = blink::mojom::SpeculationAction::kPrerender;
    candidate->url = url;
    candidate->referrer = blink::mojom::Referrer::New();
    candidate->eagerness = blink::mojom::SpeculationEagerness::kImmediate;
    candidate->tags = {std::nullopt};
    return candidate;
  }

  blink::mojom::SpeculationCandidatePtr CreatePrerenderCandidateWithEagerness(
      const GURL& url,
      blink::mojom::SpeculationEagerness eagerness) {
    blink::mojom::SpeculationCandidatePtr candidate =
        CreatePrerenderCandidate(url);
    candidate->eagerness = eagerness;
    return candidate;
  }

 private:
  test::ScopedPrerenderFeatureList prerender_feature_list_;
  std::unique_ptr<TestBrowserContext> browser_context_;
  std::unique_ptr<TestWebContents> web_contents_;
  std::unique_ptr<test::ScopedPrerenderWebContentsDelegate>
      web_contents_delegate_;
};

// Tests that Prerenderer starts prerendering when it receives prerender
// speculation candidates.
TEST_F(PrerendererTest, StartPrerender) {
  PrerenderHostRegistry* registry = GetPrerenderHostRegistry();
  PrerendererImpl prerenderer(*GetRenderFrameHost());

  const GURL kPrerenderingUrl = GetSameOriginUrl("/empty.html");
  std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
  candidates.push_back(CreatePrerenderCandidate(kPrerenderingUrl));

  prerenderer.ProcessCandidatesForPrerender(std::move(candidates));
  EXPECT_TRUE(registry->FindHostByUrlForTesting(kPrerenderingUrl));
}

// Tests that Prerenderer should not start prerendering when
// kLCPTimingPredictorPrerender2 is enabled and until OnLCPPredicted is called.
TEST_F(PrerendererTest, LCPTimingPredictorPrerender2) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndEnableFeature(
      blink::features::kLCPTimingPredictorPrerender2);

  PrerenderHostRegistry* registry = GetPrerenderHostRegistry();
  PrerendererImpl prerenderer(*GetRenderFrameHost());

  const GURL kPrerenderingUrl = GetSameOriginUrl("/empty.html");
  std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
  candidates.push_back(CreatePrerenderCandidate(kPrerenderingUrl));

  prerenderer.ProcessCandidatesForPrerender(std::move(candidates));
  EXPECT_FALSE(registry->FindHostByUrlForTesting(kPrerenderingUrl));

  prerenderer.OnLCPPredicted();
  EXPECT_TRUE(registry->FindHostByUrlForTesting(kPrerenderingUrl));
}

// Tests that Prerenderer will skip a cross-site candidate even if it is the
// first prerender candidate in the candidate list.
TEST_F(PrerendererTest, ProcessFirstSameOriginPrerenderCandidate) {
  PrerenderHostRegistry* registry = GetPrerenderHostRegistry();
  PrerendererImpl prerenderer(*GetRenderFrameHost());

  const GURL kFirstPrerenderingUrlCrossSite = GetCrossSiteUrl("/title.html");
  const GURL kSecondPrerenderingUrlSameOrigin =
      GetSameOriginUrl("/title1.html");
  std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
  candidates.push_back(
      CreatePrerenderCandidate(kFirstPrerenderingUrlCrossSite));
  candidates.push_back(
      CreatePrerenderCandidate(kSecondPrerenderingUrlSameOrigin));

  prerenderer.ProcessCandidatesForPrerender(std::move(candidates));

  // The first prerender candidate is a cross-site one, so Prerenderer should
  // not prerender it.
  EXPECT_FALSE(
      registry->FindHostByUrlForTesting(kFirstPrerenderingUrlCrossSite));
  // The second element in this list is the first same-origin prerender
  // candidate, so Prerenderer should prerender this candidate.
  EXPECT_TRUE(
      registry->FindHostByUrlForTesting(kSecondPrerenderingUrlSameOrigin));
}

class PrerenderHostRegistryObserver : public PrerenderHostRegistry::Observer {
 public:
  explicit PrerenderHostRegistryObserver(
      PrerenderHostRegistry* prerender_host_registry) {
    observation_.Observe(prerender_host_registry);
  }

  void OnTrigger(const GURL& url) override { trigger_sequence_.push_back(url); }

  void OnRegistryDestroyed() override { observation_.Reset(); }

  const std::vector<GURL>& GetTriggerSequence() { return trigger_sequence_; }

 private:
  base::ScopedObservation<PrerenderHostRegistry,
                          PrerenderHostRegistry::Observer>
      observation_{this};

  std::vector<GURL> trigger_sequence_;
};

// Test that ProcessCandidatesForPrerender will trigger candidates in the same
// order as the input of the candidate list (crbug.com/1505301).
TEST_F(PrerendererTest, TriggerPrerenderWithInsertionOrder) {
  PrerenderHostRegistry* registry = GetPrerenderHostRegistry();
  PrerenderHostRegistryObserver observer{registry};
  PrerendererImpl prerenderer(*GetRenderFrameHost());

  std::vector<GURL> urls = {
      GetSameOriginUrl("/empty.html?a"),
      GetSameOriginUrl("/empty.html?c"),
      GetSameOriginUrl("/empty.html?b"),
  };

  std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
  for (const auto& url : urls) {
    candidates.push_back(CreatePrerenderCandidate(url));
  }

  prerenderer.ProcessCandidatesForPrerender(std::move(candidates));

  for (const auto& url : urls) {
    ASSERT_TRUE(registry->FindHostByUrlForTesting(url));
  }
  EXPECT_EQ(observer.GetTriggerSequence(), urls);
}

// Tests that Prerenderer will remove the rendered host, if the url is removed
// from candidates list.
TEST_F(PrerendererTest, RemoveRendererHostAfterCandidateRemoved) {
  PrerenderHostRegistry* registry = GetPrerenderHostRegistry();
  PrerendererImpl prerenderer(*GetRenderFrameHost());

  const auto urls = std::to_array<GURL>(
      {GetSameOriginUrl("/title1.html"), GetSameOriginUrl("/title2.html")});
  std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
  for (const auto& url : urls) {
    candidates.push_back(CreatePrerenderCandidate(url));
  }
  prerenderer.ProcessCandidatesForPrerender(std::move(candidates));
  EXPECT_TRUE(registry->FindHostByUrlForTesting(urls[0]));
  EXPECT_TRUE(registry->FindHostByUrlForTesting(urls[1]));

  std::vector<blink::mojom::SpeculationCandidatePtr> new_candidates;
  new_candidates.push_back(CreatePrerenderCandidate(urls[1]));
  prerenderer.ProcessCandidatesForPrerender(std::move(new_candidates));
  EXPECT_FALSE(registry->FindHostByUrlForTesting(urls[0]));
  EXPECT_TRUE(registry->FindHostByUrlForTesting(urls[1]));

  prerenderer.ProcessCandidatesForPrerender(
      std::vector<blink::mojom::SpeculationCandidatePtr>{});
  EXPECT_FALSE(registry->FindHostByUrlForTesting(urls[0]));
  EXPECT_FALSE(registry->FindHostByUrlForTesting(urls[1]));
}

// Tests that Prerenderer will remove the host if the host is canceled with
// non-immediate limit, and the canceled host can be reprocessed.
TEST_F(PrerendererTest, RemoveRendererHostAfterNonImmediateLimitCancel) {
  PrerenderHostRegistry* registry = GetPrerenderHostRegistry();
  PrerendererImpl prerenderer(*GetRenderFrameHost());

  std::vector<GURL> urls;

  // Prerender as many times as limit + 1. All prerenders should be started
  // once.
  for (int i = 0;
       i < PrerenderHostRegistry::
                   kMaxRunningSpeculationRulesNonImmediatePrerenders +
               1;
       i++) {
    const GURL url = GetSameOriginUrl("/empty.html?" + base::ToString(i));
    urls.push_back(url);
    blink::mojom::SpeculationCandidatePtr candidate =
        CreatePrerenderCandidateWithEagerness(
            url, blink::mojom::SpeculationEagerness::kConservative);
    prerenderer.MaybePrerender(std::move(candidate),
                               preloading_predictor::kUnspecified,
                               PreloadingConfidence{100});

    EXPECT_TRUE(registry->FindHostByUrlForTesting(url));
  }

  for (int i = 0;
       i < PrerenderHostRegistry::
                   kMaxRunningSpeculationRulesNonImmediatePrerenders +
               1;
       i++) {
    if (i == 0) {
      // The first (= oldest) prerender should be removed since the (limit +
      // 1)-th prerender was started.
      EXPECT_FALSE(registry->FindHostByUrlForTesting(urls[i]));
    } else {
      EXPECT_TRUE(registry->FindHostByUrlForTesting(urls[i]));
    }
  }

  // Retrigger canceled host. It should be started and instead the second oldest
  // prerender should be canceled.
  blink::mojom::SpeculationCandidatePtr candidate =
      CreatePrerenderCandidateWithEagerness(
          urls[0], blink::mojom::SpeculationEagerness::kConservative);
  prerenderer.MaybePrerender(std::move(candidate),
                             preloading_predictor::kUnspecified,
                             PreloadingConfidence{100});
  for (int i = 0;
       i < PrerenderHostRegistry::
                   kMaxRunningSpeculationRulesNonImmediatePrerenders +
               1;
       i++) {
    if (i == 1) {
      EXPECT_FALSE(registry->FindHostByUrlForTesting(urls[i]));
    } else {
      EXPECT_TRUE(registry->FindHostByUrlForTesting(urls[i]));
    }
  }
}

// Tests that it is possible to start a prerender using MaybePrerender and
// ShouldWaitForPrerenderResult methods.
TEST_F(PrerendererTest, MaybePrerenderAndShouldWaitForPrerenderResult) {
  PrerenderHostRegistry* registry = GetPrerenderHostRegistry();
  PrerendererImpl prerenderer(*GetRenderFrameHost());

  const GURL kUrlToCancel = GetSameOriginUrl("/to_cancel.html");
  std::vector<blink::mojom::SpeculationCandidatePtr> candidateToCancel;
  candidateToCancel.push_back(CreatePrerenderCandidate(kUrlToCancel));

  // Candidate is not processed yet. So, it should return false.
  EXPECT_FALSE(prerenderer.ShouldWaitForPrerenderResult(kUrlToCancel));
  // Process the candidate.
  prerenderer.ProcessCandidatesForPrerender(std::move(candidateToCancel));
  EXPECT_TRUE(prerenderer.ShouldWaitForPrerenderResult(kUrlToCancel));
  // Cancel the prerender
  prerenderer.ProcessCandidatesForPrerender(
      std::vector<blink::mojom::SpeculationCandidatePtr>{});
  EXPECT_FALSE(prerenderer.ShouldWaitForPrerenderResult(kUrlToCancel));

  const GURL kPrerenderingUrl = GetSameOriginUrl("/empty.html");
  const auto candidate = CreatePrerenderCandidate(kPrerenderingUrl);

  // Candidate is not processed yet. So, it should return false.
  EXPECT_FALSE(prerenderer.ShouldWaitForPrerenderResult(kPrerenderingUrl));
  // MaybePrerender the candidate and check if ShouldWaitForPrerenderResult
  // returns true.
  EXPECT_TRUE(prerenderer.MaybePrerender(candidate,
                                         preloading_predictor::kUnspecified,
                                         PreloadingConfidence{100}));
  EXPECT_TRUE(prerenderer.ShouldWaitForPrerenderResult(kPrerenderingUrl));
  EXPECT_TRUE(registry->FindHostByUrlForTesting(kPrerenderingUrl));
}

}  // namespace
}  // namespace content