#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";
}
class FetchLaterBrowserTestBase : public KeepAliveRequestBrowserTestBase {
protected:
void SetUp() override {
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_);
return previous_document_->WaitUntilRenderFrameDeleted();
}
RenderFrameHostImplWrapper& previous_document() {
CHECK(previous_document_);
CHECK(!previous_document_->IsDestroyed());
return *previous_document_;
}
RenderFrameHostImplWrapper& current_document() {
CHECK(previous_document_);
return *current_document_;
}
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()));
}
void RunScriptAndNavigateAway(const std::string& script) {
RunScript(script);
ASSERT_TRUE(
NavigateToURL(server()->GetURL(kSecondaryHost, "/title2.html")));
ASSERT_TRUE(WaitForLoadStop(web_contents()));
}
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) {
handler->WaitForRequest();
handler->Send(k200TextResponse);
handler->Done();
}
loaders_observer().WaitForTotalOnReceiveResponse(total);
}
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());
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"));
EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
}
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"));
EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
}
struct TestTimeoutType {
std::string test_case_name;
int32_t timeout;
};
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},
{"OneMinuteTimeout", 60000},
}),
[](const testing::TestParamInfo<TestTimeoutType>& info) {
return info.param.test_case_name;
});
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());
RunScriptAndNavigateAway(JsReplace(R"(
fetchLater($1, {activateAfter: $2});
fetchLater($1, {activateAfter: $2});
)",
target_url, GetParam().timeout));
ASSERT_TRUE(WaitUntilPreviousDocumentDeleted());
EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 2u);
ExpectFetchLaterRequests(2, request_handlers);
}
class FetchLaterWithBackForwardCacheMetricsBrowserTestBase
: public FetchLaterBrowserTestBase,
public BackForwardCacheMetricsTestMatcher {
protected:
void SetUpOnMainThread() override {
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();
}
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_;
};
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"}}},
{features::kBackForwardCacheMemoryControls,
{{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}};
return enabled_features;
}
};
IN_PROC_BROWSER_TEST_F(FetchLaterNoActivationTimeoutBrowserTest,
SendOnPageDeletion) {
const std::string target_url = kFetchLaterEndpoint;
auto request_handlers = RegisterRequestHandlers({target_url});
ASSERT_TRUE(server()->Start());
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)));
EXPECT_EQ(true, EvalJs(web_contents(), "promise"));
EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 1u);
ExpectFetchLaterRequests(1, request_handlers);
}
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());
ASSERT_TRUE(HistoryGoBack(web_contents()));
ExpectRestored(FROM_HERE);
ExpectFetchLaterRequests(1, request_handlers);
}
IN_PROC_BROWSER_TEST_F(FetchLaterNoActivationTimeoutBrowserTest,
NotSendWhenPageIsResumedAfterBeingFrozen) {
const std::string target_url = kFetchLaterEndpoint;
ASSERT_TRUE(server()->Start());
ASSERT_TRUE(NavigateToURL(server()->GetURL(kPrimaryHost, "/title1.html")));
ASSERT_TRUE(ExecJs(web_contents(), JsReplace(R"(
fetchLater($1);
)",
target_url)));
ASSERT_TRUE(WaitForLoadStop(web_contents()));
web_contents()->WasHidden();
web_contents()->SetPageFrozen(true);
EXPECT_EQ(loader_service()->NumLoadersForTesting(), 1u);
EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
web_contents()->WasHidden();
web_contents()->SetPageFrozen(false);
EXPECT_EQ(loader_service()->NumLoadersForTesting(), 1u);
EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
}
class FetchLaterActivationTimeoutBrowserTest
: public FetchLaterWithBackForwardCacheMetricsBrowserTestBase {
protected:
const FeaturesType& GetEnabledFeatures() override {
static const FeaturesType enabled_features = {
{blink::features::kFetchLaterAPI, {}},
{features::kBackForwardCache, {{}}},
{features::kBackForwardCacheTimeToLiveControl,
{{"time_to_live_seconds", "60"}}},
{features::kBackForwardCacheMemoryControls,
{{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}};
return enabled_features;
}
};
IN_PROC_BROWSER_TEST_F(FetchLaterActivationTimeoutBrowserTest,
SendOnZeroActivationTimeout) {
const std::string target_url = kFetchLaterEndpoint;
auto request_handlers = RegisterRequestHandlers({target_url});
ASSERT_TRUE(server()->Start());
RunScript(JsReplace(R"(
fetchLater($1, {activateAfter: 0});
)",
target_url));
ASSERT_FALSE(current_document().IsDestroyed());
EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
ExpectFetchLaterRequests(1, request_handlers);
}
IN_PROC_BROWSER_TEST_F(FetchLaterActivationTimeoutBrowserTest,
SendOnActivationTimeout) {
const std::string target_url = kFetchLaterEndpoint;
auto request_handlers = RegisterRequestHandlers({target_url});
ASSERT_TRUE(server()->Start());
RunScript(JsReplace(R"(
fetchLater($1, {activateAfter: 2000});
)",
target_url));
ASSERT_FALSE(current_document().IsDestroyed());
EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
ExpectFetchLaterRequests(1, request_handlers);
}
IN_PROC_BROWSER_TEST_F(FetchLaterActivationTimeoutBrowserTest,
SendOnBackForwardCachedEviction) {
const std::string target_url = kFetchLaterEndpoint;
auto request_handlers = RegisterRequestHandlers({target_url});
ASSERT_TRUE(server()->Start());
RunScriptAndNavigateAway(JsReplace(R"(
fetchLater($1, {activateAfter: 180000});
)",
target_url));
ASSERT_TRUE(previous_document()->IsInBackForwardCache());
DisableBFCacheForRFHForTesting(previous_document()->GetGlobalId());
ASSERT_TRUE(previous_document()->is_evicted_from_back_forward_cache());
ASSERT_TRUE(previous_document().WaitUntilRenderFrameDeleted());
ExpectFetchLaterRequests(1, request_handlers);
}
}