910e62b5创建于 1月15日历史提交
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "base/strings/stringprintf.h"
#include "content/browser/back_forward_cache_test_util.h"
#include "content/browser/loader/keep_alive_request_browsertest_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/keep_alive_url_loader_utils.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"

namespace content {
namespace {

constexpr char kFetchLaterEndpoint[] = "/fetch-later";

}  // namespace

// A base class to help testing JS fetchLater() API behaviors.
class FetchLaterBrowserTestBase : public KeepAliveRequestBrowserTestBase {
 protected:
  void SetUp() override {
    // fetchLater() API only supports HTTPS requests.
    SetUseHttps();
    KeepAliveRequestBrowserTestBase::SetUp();
  }

  bool NavigateToURL(const GURL& url) {
    previous_document_ =
        std::make_unique<RenderFrameHostImplWrapper>(current_frame_host());
    bool ret = content::NavigateToURL(web_contents(), url);
    current_document_ =
        std::make_unique<RenderFrameHostImplWrapper>(current_frame_host());
    return ret;
  }
  bool WaitUntilPreviousDocumentDeleted() {
    CHECK(previous_document_);
    // `previous_document_` might already be destroyed here.
    return previous_document_->WaitUntilRenderFrameDeleted();
  }
  // Caution: the returned document might already be killed if BFCache is not
  // working.
  RenderFrameHostImplWrapper& previous_document() {
    CHECK(previous_document_);
    CHECK(!previous_document_->IsDestroyed());
    return *previous_document_;
  }
  RenderFrameHostImplWrapper& current_document() {
    CHECK(previous_document_);
    return *current_document_;
  }

  // Navigates to an empty page, and executes `script` on it.
  void RunScript(const std::string& script) {
    ASSERT_TRUE(NavigateToURL(server()->GetURL(kPrimaryHost, "/title1.html")));
    ASSERT_TRUE(ExecJs(web_contents(), script));
    ASSERT_TRUE(WaitForLoadStop(web_contents()));
  }

  // Navigates to a page that executes `script`, and navigates to another page.
  void RunScriptAndNavigateAway(const std::string& script) {
    RunScript(script);

    // Navigate to cross-origin page to ensure the 1st page can be unloaded if
    // BackForwardCache is disabled.
    ASSERT_TRUE(
        NavigateToURL(server()->GetURL(kSecondaryHost, "/title2.html")));
    ASSERT_TRUE(WaitForLoadStop(web_contents()));
  }

  // Expects `total` number of FetchLater requests to be sent.
  // `total` must equal to the size of `request_handlers`.
  // `requester_handlers` are to wait for the FetchLater requests and to
  // respond.
  void ExpectFetchLaterRequests(
      size_t total,
      std::vector<std::unique_ptr<net::test_server::ControllableHttpResponse>>&
          request_handlers) {
    SCOPED_TRACE(
        base::StringPrintf("ExpectFetchLaterRequests: %zu requests", total));
    ASSERT_EQ(total, request_handlers.size());
    EXPECT_EQ(loader_service()->NumLoadersForTesting(), total);

    for (const auto& handler : request_handlers) {
      // Waits for a FetchLater request.
      handler->WaitForRequest();
      // Sends back final response to terminate in-browser request handling.
      handler->Send(k200TextResponse);
      // Triggers OnComplete.
      handler->Done();
    }

    loaders_observer().WaitForTotalOnReceiveResponse(total);
    // TODO(crbug.com/40236167): Check NumLoadersForTesting==0 after migrating
    // to in-browser ThrottlingURLLoader. Current implementation cannot ensure
    // receiving renderer disconnection. Also need to wait for TotalOnComplete
    // by `total`, not by states.
  }

  GURL GetFetchLaterPageURL(const std::string& host,
                            const std::string& method) const {
    std::string url = base::StrCat(
        {"/set-header-with-file/content/test/data/fetch_later.html?"
         "method=",
         method});
    return server()->GetURL(host, url);
  }

 private:
  std::unique_ptr<RenderFrameHostImplWrapper> current_document_ = nullptr;
  std::unique_ptr<RenderFrameHostImplWrapper> previous_document_ = nullptr;
};

class FetchLaterBasicBrowserTest : public FetchLaterBrowserTestBase {
 protected:
  const FeaturesType& GetEnabledFeatures() override {
    static const FeaturesType enabled_features = {
        {blink::features::kFetchLaterAPI, {{}}}};
    return enabled_features;
  }
};

IN_PROC_BROWSER_TEST_F(FetchLaterBasicBrowserTest, CallInMainDocument) {
  const std::string target_url = kFetchLaterEndpoint;
  ASSERT_TRUE(server()->Start());

  RunScript(JsReplace(R"(
    fetchLater($1);
  )",
                      target_url));
  ASSERT_FALSE(current_document().IsDestroyed());

  // The loader should still be connected as the page exists.
  EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
}

IN_PROC_BROWSER_TEST_F(FetchLaterBasicBrowserTest, CallInSameOriginChild) {
  ASSERT_TRUE(server()->Start());

  RunScript(JsReplace(
      R"(
    var childPromise = new Promise((resolve, reject) => {
      window.addEventListener('message', e => {
        if (e.data.type === 'fetchLater.done') {
          resolve(e.data.type);
        } else {
          reject(e.data.type);
        }
      });
    });

    const iframe = document.createElement("iframe");
    iframe.src = $1;
    document.body.appendChild(iframe);
  )",
      GetFetchLaterPageURL(kPrimaryHost, net::HttpRequestHeaders::kGetMethod)));
  ASSERT_FALSE(current_document().IsDestroyed());

  EXPECT_EQ("fetchLater.done", EvalJs(web_contents(), "childPromise"));
  // The loader should still be connected as the page exists.
  EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
}

// By default of `deferred-fetch-minimal` policy, `fetchLater()` should be
// allowed in first X cross-origin child iframes.
IN_PROC_BROWSER_TEST_F(FetchLaterBasicBrowserTest, CallInCrossOriginChild) {
  const std::string target_url = kFetchLaterEndpoint;
  ASSERT_TRUE(server()->Start());

  RunScript(JsReplace(
      R"(
    var childPromise = new Promise((resolve, reject) => {
      window.addEventListener('message', e => {
        if (e.data.type === 'fetchLater.done') {
          resolve(e.data.type);
        } else {
          reject(e.data.type + ': ' + e.data.error);
        }
      });
    });

    const iframe = document.createElement("iframe");
    iframe.src = $1;
    document.body.appendChild(iframe);
  )",
      GetFetchLaterPageURL(kSecondaryHost,
                           net::HttpRequestHeaders::kGetMethod)));
  ASSERT_FALSE(current_document().IsDestroyed());

  EXPECT_EQ("fetchLater.done", EvalJs(web_contents(), "childPromise"));
  // The loader should still exist as the page exists.
  EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
}

// A type to support parameterized testing for timeout-related tests.
struct TestTimeoutType {
  std::string test_case_name;
  int32_t timeout;
};

// Tests to cover FetchLater's behaviors when BackForwardCache is off.
//
// Disables BackForwardCache such that a page is discarded right away on user
// navigating to another page.
class FetchLaterNoBackForwardCacheBrowserTest
    : public FetchLaterBrowserTestBase,
      public testing::WithParamInterface<TestTimeoutType> {
 protected:
  const FeaturesType& GetEnabledFeatures() override {
    static const FeaturesType enabled_features = {
        {blink::features::kFetchLaterAPI, {{}}}};
    return enabled_features;
  }
  const DisabledFeaturesType& GetDisabledFeatures() override {
    static const DisabledFeaturesType disabled_features = {
        features::kBackForwardCache};
    return disabled_features;
  }
};

INSTANTIATE_TEST_SUITE_P(
    All,
    FetchLaterNoBackForwardCacheBrowserTest,
    testing::ValuesIn<std::vector<TestTimeoutType>>({
        {"LongTimeout", 600000},      // 10 minutes
        {"OneMinuteTimeout", 60000},  // 1 minute
    }),
    [](const testing::TestParamInfo<TestTimeoutType>& info) {
      return info.param.test_case_name;
    });

// All pending FetchLater requests should be sent after the initiator page is
// gone, no matter how much time their activateAfter has left.
// Disables BackForwardCache such that a page is discarded right away on user
// navigating to another page.
IN_PROC_BROWSER_TEST_P(FetchLaterNoBackForwardCacheBrowserTest,
                       SendOnPageDiscardBeforeActivationTimeout) {
  const std::string target_url = kFetchLaterEndpoint;
  auto request_handlers = RegisterRequestHandlers({target_url, target_url});
  ASSERT_TRUE(server()->Start());

  // Creates two FetchLater requests with various long activateAfter, which
  // should all be sent on page discard.
  RunScriptAndNavigateAway(JsReplace(R"(
    fetchLater($1, {activateAfter: $2});
    fetchLater($1, {activateAfter: $2});
  )",
                                     target_url, GetParam().timeout));
  // Ensure the 1st page has been unloaded.
  ASSERT_TRUE(WaitUntilPreviousDocumentDeleted());

  // Loaders are disconnected after the 1st page is gone.
  EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 2u);
  // The FetchLater requests should've been sent after the 1st page is gone.
  ExpectFetchLaterRequests(2, request_handlers);
}

class FetchLaterWithBackForwardCacheMetricsBrowserTestBase
    : public FetchLaterBrowserTestBase,
      public BackForwardCacheMetricsTestMatcher {
 protected:
  void SetUpOnMainThread() override {
    // TestAutoSetUkmRecorder's constructor requires a sequenced context.
    ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
    histogram_tester_ = std::make_unique<base::HistogramTester>();
    FetchLaterBrowserTestBase::SetUpOnMainThread();
  }

  void TearDownOnMainThread() override {
    ukm_recorder_.reset();
    histogram_tester_.reset();
    FetchLaterBrowserTestBase::TearDownOnMainThread();
  }

  // `BackForwardCacheMetricsTestMatcher` implementation.
  const ukm::TestAutoSetUkmRecorder& ukm_recorder() override {
    return *ukm_recorder_;
  }
  const base::HistogramTester& histogram_tester() override {
    return *histogram_tester_;
  }

 private:
  std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_;
  std::unique_ptr<base::HistogramTester> histogram_tester_;
};

// Tests to cover FetchLater's behaviors when BackForwardCache is on but does
// not come into play.
//
// Setting long `BackForwardCache TTL (1min)` so that FetchLater sending cannot
// be caused by page eviction out of BackForwardCache.
class FetchLaterNoActivationTimeoutBrowserTest
    : public FetchLaterWithBackForwardCacheMetricsBrowserTestBase {
 protected:
  const FeaturesType& GetEnabledFeatures() override {
    static const FeaturesType enabled_features = {
        {blink::features::kFetchLaterAPI, {}},
        {features::kBackForwardCache, {{}}},
        {features::kBackForwardCacheTimeToLiveControl,
         {{"time_to_live_seconds", "60"}}},
        // Forces BackForwardCache to work in low memory device.
        {features::kBackForwardCacheMemoryControls,
         {{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}};
    return enabled_features;
  }
};

// A pending FetchLater request with default options should be sent after the
// initiator page is gone.
// Similar to SendOnPageDiscardBeforeActivationTimeout.
IN_PROC_BROWSER_TEST_F(FetchLaterNoActivationTimeoutBrowserTest,
                       SendOnPageDeletion) {
  const std::string target_url = kFetchLaterEndpoint;
  auto request_handlers = RegisterRequestHandlers({target_url});
  ASSERT_TRUE(server()->Start());

  // Creates a FetchLater request in an iframe, which is removed after loaded.
  ASSERT_TRUE(NavigateToURL(
      server()->GetURL(kPrimaryHost, "/page_with_blank_iframe.html")));
  ASSERT_TRUE(ExecJs(web_contents(), R"(
    var promise = new Promise(resolve => {
      window.addEventListener('message', e => {
        const iframe = document.getElementById('test_iframe');
        iframe.remove();
        resolve(e.data);
      });
    });
  )"));
  auto* iframe =
      static_cast<RenderFrameHostImpl*>(ChildFrameAt(web_contents(), 0));
  EXPECT_TRUE(ExecJs(iframe, JsReplace(R"(
      fetchLater($1);
      window.parent.postMessage(true, "*");
    )",
                                       target_url)));
  // `iframe` is removed after it calls fetchLater().
  EXPECT_EQ(true, EvalJs(web_contents(), "promise"));

  // The loader is disconnected after the 1st page is gone.
  EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 1u);
  // The FetchLater requests should've been sent after the 1st page is gone.
  ExpectFetchLaterRequests(1, request_handlers);
}

// A pending FetchLater request should have been sent after its page gets
// restored from BackForwardCache before getting evicted. It is because, by
// default, pending requests are all flushed on BFCache no matter
// BackgroundSync is on or not. See http://crbug.com/310541607#comment28.
IN_PROC_BROWSER_TEST_F(
    FetchLaterNoActivationTimeoutBrowserTest,
    FlushedWhenPageIsRestoredBeforeBeingEvictedFromBackForwardCache) {
  const std::string target_url = kFetchLaterEndpoint;
  auto request_handlers = RegisterRequestHandlers({target_url});
  ASSERT_TRUE(server()->Start());

  RunScriptAndNavigateAway(JsReplace(R"(
    fetchLater($1);
  )",
                                     target_url));
  ASSERT_TRUE(previous_document()->IsInBackForwardCache());
  // Navigate back to the 1st page.
  ASSERT_TRUE(HistoryGoBack(web_contents()));

  // The same page is still alive.
  ExpectRestored(FROM_HERE);
  // The FetchLater requests should've been sent.
  ExpectFetchLaterRequests(1, request_handlers);
}

// Without an activateAfter set, a pending FetchLater request should not be
// sent out during its page frozen state.
// Similar to ResetActivationTimeoutTimerOnPageResume.
IN_PROC_BROWSER_TEST_F(FetchLaterNoActivationTimeoutBrowserTest,
                       NotSendWhenPageIsResumedAfterBeingFrozen) {
  const std::string target_url = kFetchLaterEndpoint;
  ASSERT_TRUE(server()->Start());

  // Creates a FetchLater request with NO activateAfter.
  // It should be impossible to send out during page frozen.
  ASSERT_TRUE(NavigateToURL(server()->GetURL(kPrimaryHost, "/title1.html")));
  ASSERT_TRUE(ExecJs(web_contents(), JsReplace(R"(
    fetchLater($1);
  )",
                                               target_url)));
  ASSERT_TRUE(WaitForLoadStop(web_contents()));

  // Forces to freeze the current page.
  web_contents()->WasHidden();
  web_contents()->SetPageFrozen(true);

  // The FetchLater request should not be sent.
  EXPECT_EQ(loader_service()->NumLoadersForTesting(), 1u);
  EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);

  // Forces to wake up the current page.
  web_contents()->WasHidden();
  web_contents()->SetPageFrozen(false);
  // The FetchLater request should not be sent.
  // TODO(crbug.com/40276121): Verify FetchLaterResult once
  // https://crrev.com/c/4820528 is submitted.
  EXPECT_EQ(loader_service()->NumLoadersForTesting(), 1u);
  EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
}

// Tests to cover FetchLater's activateAfter behaviors when BackForwardCache
// is on and may come into play.
//
// BackForwardCache eviction is simulated by calling
// `DisableBFCacheForRFHForTesting(previous_document())` instead of relying on
// its TTL.
class FetchLaterActivationTimeoutBrowserTest
    : public FetchLaterWithBackForwardCacheMetricsBrowserTestBase {
 protected:
  const FeaturesType& GetEnabledFeatures() override {
    static const FeaturesType enabled_features = {
        {blink::features::kFetchLaterAPI, {}},
        {features::kBackForwardCache, {{}}},
        // Sets to a long timeout, as tests below should not rely on it.
        {features::kBackForwardCacheTimeToLiveControl,
         {{"time_to_live_seconds", "60"}}},
        // Forces BackForwardCache to work in low memory device.
        {features::kBackForwardCacheMemoryControls,
         {{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}};
    return enabled_features;
  }
};

// When setting activateAfter=0, a pending FetchLater request should be sent
// "roughly" immediately.
IN_PROC_BROWSER_TEST_F(FetchLaterActivationTimeoutBrowserTest,
                       SendOnZeroActivationTimeout) {
  const std::string target_url = kFetchLaterEndpoint;
  auto request_handlers = RegisterRequestHandlers({target_url});
  ASSERT_TRUE(server()->Start());

  // Creates a FetchLater request with activateAfter=0s.
  RunScript(JsReplace(R"(
    fetchLater($1, {activateAfter: 0});
  )",
                      target_url));
  ASSERT_FALSE(current_document().IsDestroyed());

  // The loader should still exist as the page exists.
  EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
  // The FetchLater request should be sent, triggered by its activateAfter.
  ExpectFetchLaterRequests(1, request_handlers);
}

// When setting activateAfter>0, a pending FetchLater request should be sent
// after around the specified time, if no navigation happens.
IN_PROC_BROWSER_TEST_F(FetchLaterActivationTimeoutBrowserTest,
                       SendOnActivationTimeout) {
  const std::string target_url = kFetchLaterEndpoint;
  auto request_handlers = RegisterRequestHandlers({target_url});
  ASSERT_TRUE(server()->Start());

  // Creates a FetchLater request with activateAfter=2s.
  // It should be sent out after 2s.
  RunScript(JsReplace(R"(
    fetchLater($1, {activateAfter: 2000});
  )",
                      target_url));
  ASSERT_FALSE(current_document().IsDestroyed());

  // The loader should still exist as the page exists.
  EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
  // The FetchLater request should be sent, triggered by its activateAfter.
  ExpectFetchLaterRequests(1, request_handlers);
}

// A pending FetchLater request should be sent when its page is evicted out of
// BackForwardCache.
IN_PROC_BROWSER_TEST_F(FetchLaterActivationTimeoutBrowserTest,
                       SendOnBackForwardCachedEviction) {
  const std::string target_url = kFetchLaterEndpoint;
  auto request_handlers = RegisterRequestHandlers({target_url});
  ASSERT_TRUE(server()->Start());

  // Creates a FetchLater request with long activateAfter (3min)
  RunScriptAndNavigateAway(JsReplace(R"(
    fetchLater($1, {activateAfter: 180000});
  )",
                                     target_url));
  ASSERT_TRUE(previous_document()->IsInBackForwardCache());
  // Forces evicting previous page. This will also post a task that destroys it.
  DisableBFCacheForRFHForTesting(previous_document()->GetGlobalId());
  ASSERT_TRUE(previous_document()->is_evicted_from_back_forward_cache());
  // Eviction happens immediately, but RFH deletion may be delayed.
  ASSERT_TRUE(previous_document().WaitUntilRenderFrameDeleted());

  // The loader is disconnected after the page is evicted by browser process to
  // start loading the request. However, it may happen earlier or later, so it's
  // difficult to assert the existence of the disconnected loader.

  // At the end, the FetchLater request should be sent, and the loader is
  // expected to process the response.
  ExpectFetchLaterRequests(1, request_handlers);
}

// All other send-on-BFCache behaviors are covered in
// send-on-deactivate.tentative.https.window.js

}  // namespace content