blob: 8b671e1de225d563dde08d12122414227f31c23f [file] [log] [blame]
Ming-Ying Chung740c1642025-04-17 09:00:481// Copyright 2025 The Chromium Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include <memory>
6#include <optional>
7#include <string>
8#include <vector>
9
10#include "base/strings/stringprintf.h"
11#include "content/browser/back_forward_cache_test_util.h"
12#include "content/browser/loader/keep_alive_request_browsertest_util.h"
13#include "content/public/test/browser_test.h"
14#include "content/public/test/browser_test_utils.h"
15#include "content/public/test/keep_alive_url_loader_utils.h"
16#include "content/test/content_browser_test_utils_internal.h"
17#include "net/test/embedded_test_server/controllable_http_response.h"
18#include "testing/gtest/include/gtest/gtest.h"
19#include "third_party/blink/public/common/features.h"
20#include "url/gurl.h"
21
22namespace content {
23namespace {
24
25constexpr char kFetchLaterEndpoint[] = "/fetch-later";
26
27} // namespace
28
29// A base class to help testing JS fetchLater() API behaviors.
30class FetchLaterBrowserTestBase : public KeepAliveRequestBrowserTestBase {
31 protected:
32 void SetUp() override {
33 // fetchLater() API only supports HTTPS requests.
34 SetUseHttps();
35 KeepAliveRequestBrowserTestBase::SetUp();
36 }
37
38 bool NavigateToURL(const GURL& url) {
39 previous_document_ =
40 std::make_unique<RenderFrameHostImplWrapper>(current_frame_host());
41 bool ret = content::NavigateToURL(web_contents(), url);
42 current_document_ =
43 std::make_unique<RenderFrameHostImplWrapper>(current_frame_host());
44 return ret;
45 }
46 bool WaitUntilPreviousDocumentDeleted() {
47 CHECK(previous_document_);
48 // `previous_document_` might already be destroyed here.
49 return previous_document_->WaitUntilRenderFrameDeleted();
50 }
51 // Caution: the returned document might already be killed if BFCache is not
52 // working.
53 RenderFrameHostImplWrapper& previous_document() {
54 CHECK(previous_document_);
55 CHECK(!previous_document_->IsDestroyed());
56 return *previous_document_;
57 }
58 RenderFrameHostImplWrapper& current_document() {
59 CHECK(previous_document_);
60 return *current_document_;
61 }
62
63 // Navigates to an empty page, and executes `script` on it.
64 void RunScript(const std::string& script) {
65 ASSERT_TRUE(NavigateToURL(server()->GetURL(kPrimaryHost, "/title1.html")));
66 ASSERT_TRUE(ExecJs(web_contents(), script));
67 ASSERT_TRUE(WaitForLoadStop(web_contents()));
68 }
69
70 // Navigates to a page that executes `script`, and navigates to another page.
71 void RunScriptAndNavigateAway(const std::string& script) {
72 RunScript(script);
73
74 // Navigate to cross-origin page to ensure the 1st page can be unloaded if
75 // BackForwardCache is disabled.
76 ASSERT_TRUE(
77 NavigateToURL(server()->GetURL(kSecondaryHost, "/title2.html")));
78 ASSERT_TRUE(WaitForLoadStop(web_contents()));
79 }
80
81 // Expects `total` number of FetchLater requests to be sent.
82 // `total` must equal to the size of `request_handlers`.
83 // `requester_handlers` are to wait for the FetchLater requests and to
84 // respond.
85 void ExpectFetchLaterRequests(
86 size_t total,
87 std::vector<std::unique_ptr<net::test_server::ControllableHttpResponse>>&
88 request_handlers) {
89 SCOPED_TRACE(
90 base::StringPrintf("ExpectFetchLaterRequests: %zu requests", total));
91 ASSERT_EQ(total, request_handlers.size());
92 EXPECT_EQ(loader_service()->NumLoadersForTesting(), total);
93
94 for (const auto& handler : request_handlers) {
95 // Waits for a FetchLater request.
96 handler->WaitForRequest();
97 // Sends back final response to terminate in-browser request handling.
98 handler->Send(k200TextResponse);
99 // Triggers OnComplete.
100 handler->Done();
101 }
102
103 loaders_observer().WaitForTotalOnReceiveResponse(total);
104 // TODO(crbug.com/40236167): Check NumLoadersForTesting==0 after migrating
105 // to in-browser ThrottlingURLLoader. Current implementation cannot ensure
106 // receiving renderer disconnection. Also need to wait for TotalOnComplete
107 // by `total`, not by states.
108 }
109
110 GURL GetFetchLaterPageURL(const std::string& host,
111 const std::string& method) const {
112 std::string url = base::StrCat(
113 {"/set-header-with-file/content/test/data/fetch_later.html?"
114 "method=",
115 method});
116 return server()->GetURL(host, url);
117 }
118
119 private:
120 std::unique_ptr<RenderFrameHostImplWrapper> current_document_ = nullptr;
121 std::unique_ptr<RenderFrameHostImplWrapper> previous_document_ = nullptr;
122};
123
124class FetchLaterBasicBrowserTest : public FetchLaterBrowserTestBase {
125 protected:
126 const FeaturesType& GetEnabledFeatures() override {
127 static const FeaturesType enabled_features = {
128 {blink::features::kFetchLaterAPI, {{}}}};
129 return enabled_features;
130 }
131};
132
133IN_PROC_BROWSER_TEST_F(FetchLaterBasicBrowserTest, CallInMainDocument) {
134 const std::string target_url = kFetchLaterEndpoint;
135 ASSERT_TRUE(server()->Start());
136
137 RunScript(JsReplace(R"(
138 fetchLater($1);
139 )",
140 target_url));
141 ASSERT_FALSE(current_document().IsDestroyed());
142
143 // The loader should still be connected as the page exists.
144 EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
145}
146
147IN_PROC_BROWSER_TEST_F(FetchLaterBasicBrowserTest, CallInSameOriginChild) {
148 ASSERT_TRUE(server()->Start());
149
150 RunScript(JsReplace(
151 R"(
152 var childPromise = new Promise((resolve, reject) => {
153 window.addEventListener('message', e => {
154 if (e.data.type === 'fetchLater.done') {
155 resolve(e.data.type);
156 } else {
157 reject(e.data.type);
158 }
159 });
160 });
161
162 const iframe = document.createElement("iframe");
163 iframe.src = $1;
164 document.body.appendChild(iframe);
165 )",
166 GetFetchLaterPageURL(kPrimaryHost, net::HttpRequestHeaders::kGetMethod)));
167 ASSERT_FALSE(current_document().IsDestroyed());
168
169 EXPECT_EQ("fetchLater.done", EvalJs(web_contents(), "childPromise"));
170 // The loader should still be connected as the page exists.
171 EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
172}
173
174// By default of `deferred-fetch-minimal` policy, `fetchLater()` should be
175// allowed in first X cross-origin child iframes.
176IN_PROC_BROWSER_TEST_F(FetchLaterBasicBrowserTest, CallInCrossOriginChild) {
177 const std::string target_url = kFetchLaterEndpoint;
178 ASSERT_TRUE(server()->Start());
179
180 RunScript(JsReplace(
181 R"(
182 var childPromise = new Promise((resolve, reject) => {
183 window.addEventListener('message', e => {
184 if (e.data.type === 'fetchLater.done') {
185 resolve(e.data.type);
186 } else {
187 reject(e.data.type + ': ' + e.data.error);
188 }
189 });
190 });
191
192 const iframe = document.createElement("iframe");
193 iframe.src = $1;
194 document.body.appendChild(iframe);
195 )",
196 GetFetchLaterPageURL(kSecondaryHost,
197 net::HttpRequestHeaders::kGetMethod)));
198 ASSERT_FALSE(current_document().IsDestroyed());
199
200 EXPECT_EQ("fetchLater.done", EvalJs(web_contents(), "childPromise"));
201 // The loader should still exist as the page exists.
202 EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
203}
204
205// A type to support parameterized testing for timeout-related tests.
206struct TestTimeoutType {
207 std::string test_case_name;
208 int32_t timeout;
209};
210
211// Tests to cover FetchLater's behaviors when BackForwardCache is off.
212//
213// Disables BackForwardCache such that a page is discarded right away on user
214// navigating to another page.
215class FetchLaterNoBackForwardCacheBrowserTest
216 : public FetchLaterBrowserTestBase,
217 public testing::WithParamInterface<TestTimeoutType> {
218 protected:
219 const FeaturesType& GetEnabledFeatures() override {
220 static const FeaturesType enabled_features = {
221 {blink::features::kFetchLaterAPI, {{}}}};
222 return enabled_features;
223 }
224 const DisabledFeaturesType& GetDisabledFeatures() override {
225 static const DisabledFeaturesType disabled_features = {
226 features::kBackForwardCache};
227 return disabled_features;
228 }
229};
230
231INSTANTIATE_TEST_SUITE_P(
232 All,
233 FetchLaterNoBackForwardCacheBrowserTest,
234 testing::ValuesIn<std::vector<TestTimeoutType>>({
235 {"LongTimeout", 600000}, // 10 minutes
236 {"OneMinuteTimeout", 60000}, // 1 minute
237 }),
238 [](const testing::TestParamInfo<TestTimeoutType>& info) {
239 return info.param.test_case_name;
240 });
241
242// All pending FetchLater requests should be sent after the initiator page is
243// gone, no matter how much time their activateAfter has left.
244// Disables BackForwardCache such that a page is discarded right away on user
245// navigating to another page.
246IN_PROC_BROWSER_TEST_P(FetchLaterNoBackForwardCacheBrowserTest,
247 SendOnPageDiscardBeforeActivationTimeout) {
248 const std::string target_url = kFetchLaterEndpoint;
249 auto request_handlers = RegisterRequestHandlers({target_url, target_url});
250 ASSERT_TRUE(server()->Start());
251
252 // Creates two FetchLater requests with various long activateAfter, which
253 // should all be sent on page discard.
254 RunScriptAndNavigateAway(JsReplace(R"(
255 fetchLater($1, {activateAfter: $2});
256 fetchLater($1, {activateAfter: $2});
257 )",
258 target_url, GetParam().timeout));
259 // Ensure the 1st page has been unloaded.
260 ASSERT_TRUE(WaitUntilPreviousDocumentDeleted());
261
262 // Loaders are disconnected after the 1st page is gone.
263 EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 2u);
264 // The FetchLater requests should've been sent after the 1st page is gone.
265 ExpectFetchLaterRequests(2, request_handlers);
266}
267
268class FetchLaterWithBackForwardCacheMetricsBrowserTestBase
269 : public FetchLaterBrowserTestBase,
270 public BackForwardCacheMetricsTestMatcher {
271 protected:
272 void SetUpOnMainThread() override {
273 // TestAutoSetUkmRecorder's constructor requires a sequenced context.
274 ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
275 histogram_tester_ = std::make_unique<base::HistogramTester>();
276 FetchLaterBrowserTestBase::SetUpOnMainThread();
277 }
278
279 void TearDownOnMainThread() override {
280 ukm_recorder_.reset();
281 histogram_tester_.reset();
282 FetchLaterBrowserTestBase::TearDownOnMainThread();
283 }
284
285 // `BackForwardCacheMetricsTestMatcher` implementation.
286 const ukm::TestAutoSetUkmRecorder& ukm_recorder() override {
287 return *ukm_recorder_;
288 }
289 const base::HistogramTester& histogram_tester() override {
290 return *histogram_tester_;
291 }
292
293 private:
294 std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_;
295 std::unique_ptr<base::HistogramTester> histogram_tester_;
296};
297
298// Tests to cover FetchLater's behaviors when BackForwardCache is on but does
299// not come into play.
300//
301// Setting long `BackForwardCache TTL (1min)` so that FetchLater sending cannot
302// be caused by page eviction out of BackForwardCache.
303class FetchLaterNoActivationTimeoutBrowserTest
304 : public FetchLaterWithBackForwardCacheMetricsBrowserTestBase {
305 protected:
306 const FeaturesType& GetEnabledFeatures() override {
307 static const FeaturesType enabled_features = {
308 {blink::features::kFetchLaterAPI, {}},
309 {features::kBackForwardCache, {{}}},
310 {features::kBackForwardCacheTimeToLiveControl,
311 {{"time_to_live_seconds", "60"}}},
312 // Forces BackForwardCache to work in low memory device.
313 {features::kBackForwardCacheMemoryControls,
314 {{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}};
315 return enabled_features;
316 }
317};
318
319// A pending FetchLater request with default options should be sent after the
320// initiator page is gone.
321// Similar to SendOnPageDiscardBeforeActivationTimeout.
322IN_PROC_BROWSER_TEST_F(FetchLaterNoActivationTimeoutBrowserTest,
323 SendOnPageDeletion) {
324 const std::string target_url = kFetchLaterEndpoint;
325 auto request_handlers = RegisterRequestHandlers({target_url});
326 ASSERT_TRUE(server()->Start());
327
328 // Creates a FetchLater request in an iframe, which is removed after loaded.
329 ASSERT_TRUE(NavigateToURL(
330 server()->GetURL(kPrimaryHost, "/page_with_blank_iframe.html")));
331 ASSERT_TRUE(ExecJs(web_contents(), R"(
332 var promise = new Promise(resolve => {
333 window.addEventListener('message', e => {
334 const iframe = document.getElementById('test_iframe');
335 iframe.remove();
336 resolve(e.data);
337 });
338 });
339 )"));
340 auto* iframe =
341 static_cast<RenderFrameHostImpl*>(ChildFrameAt(web_contents(), 0));
342 EXPECT_TRUE(ExecJs(iframe, JsReplace(R"(
343 fetchLater($1);
344 window.parent.postMessage(true, "*");
345 )",
346 target_url)));
347 // `iframe` is removed after it calls fetchLater().
348 EXPECT_EQ(true, EvalJs(web_contents(), "promise"));
349
350 // The loader is disconnected after the 1st page is gone.
351 EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 1u);
352 // The FetchLater requests should've been sent after the 1st page is gone.
353 ExpectFetchLaterRequests(1, request_handlers);
354}
355
356// A pending FetchLater request should have been sent after its page gets
357// restored from BackForwardCache before getting evicted. It is because, by
358// default, pending requests are all flushed on BFCache no matter
359// BackgroundSync is on or not. See http://crbug.com/310541607#comment28.
360IN_PROC_BROWSER_TEST_F(
361 FetchLaterNoActivationTimeoutBrowserTest,
362 FlushedWhenPageIsRestoredBeforeBeingEvictedFromBackForwardCache) {
363 const std::string target_url = kFetchLaterEndpoint;
364 auto request_handlers = RegisterRequestHandlers({target_url});
365 ASSERT_TRUE(server()->Start());
366
367 RunScriptAndNavigateAway(JsReplace(R"(
368 fetchLater($1);
369 )",
370 target_url));
371 ASSERT_TRUE(previous_document()->IsInBackForwardCache());
372 // Navigate back to the 1st page.
373 ASSERT_TRUE(HistoryGoBack(web_contents()));
374
375 // The same page is still alive.
376 ExpectRestored(FROM_HERE);
377 // The FetchLater requests should've been sent.
378 ExpectFetchLaterRequests(1, request_handlers);
379}
380
381// Without an activateAfter set, a pending FetchLater request should not be
382// sent out during its page frozen state.
383// Similar to ResetActivationTimeoutTimerOnPageResume.
384IN_PROC_BROWSER_TEST_F(FetchLaterNoActivationTimeoutBrowserTest,
385 NotSendWhenPageIsResumedAfterBeingFrozen) {
386 const std::string target_url = kFetchLaterEndpoint;
387 ASSERT_TRUE(server()->Start());
388
389 // Creates a FetchLater request with NO activateAfter.
390 // It should be impossible to send out during page frozen.
391 ASSERT_TRUE(NavigateToURL(server()->GetURL(kPrimaryHost, "/title1.html")));
392 ASSERT_TRUE(ExecJs(web_contents(), JsReplace(R"(
393 fetchLater($1);
394 )",
395 target_url)));
396 ASSERT_TRUE(WaitForLoadStop(web_contents()));
397
398 // Forces to freeze the current page.
399 web_contents()->WasHidden();
400 web_contents()->SetPageFrozen(true);
401
402 // The FetchLater request should not be sent.
403 EXPECT_EQ(loader_service()->NumLoadersForTesting(), 1u);
404 EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
405
406 // Forces to wake up the current page.
407 web_contents()->WasHidden();
408 web_contents()->SetPageFrozen(false);
409 // The FetchLater request should not be sent.
410 // TODO(crbug.com/40276121): Verify FetchLaterResult once
411 // https://crrev.com/c/4820528 is submitted.
412 EXPECT_EQ(loader_service()->NumLoadersForTesting(), 1u);
413 EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
414}
415
416// Tests to cover FetchLater's activateAfter behaviors when BackForwardCache
417// is on and may come into play.
418//
419// BackForwardCache eviction is simulated by calling
420// `DisableBFCacheForRFHForTesting(previous_document())` instead of relying on
421// its TTL.
422class FetchLaterActivationTimeoutBrowserTest
423 : public FetchLaterWithBackForwardCacheMetricsBrowserTestBase {
424 protected:
425 const FeaturesType& GetEnabledFeatures() override {
426 static const FeaturesType enabled_features = {
427 {blink::features::kFetchLaterAPI, {}},
428 {features::kBackForwardCache, {{}}},
429 // Sets to a long timeout, as tests below should not rely on it.
430 {features::kBackForwardCacheTimeToLiveControl,
431 {{"time_to_live_seconds", "60"}}},
432 // Forces BackForwardCache to work in low memory device.
433 {features::kBackForwardCacheMemoryControls,
434 {{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}};
435 return enabled_features;
436 }
437};
438
439// When setting activateAfter=0, a pending FetchLater request should be sent
440// "roughly" immediately.
441IN_PROC_BROWSER_TEST_F(FetchLaterActivationTimeoutBrowserTest,
442 SendOnZeroActivationTimeout) {
443 const std::string target_url = kFetchLaterEndpoint;
444 auto request_handlers = RegisterRequestHandlers({target_url});
445 ASSERT_TRUE(server()->Start());
446
447 // Creates a FetchLater request with activateAfter=0s.
448 RunScript(JsReplace(R"(
449 fetchLater($1, {activateAfter: 0});
450 )",
451 target_url));
452 ASSERT_FALSE(current_document().IsDestroyed());
453
454 // The loader should still exist as the page exists.
455 EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
456 // The FetchLater request should be sent, triggered by its activateAfter.
457 ExpectFetchLaterRequests(1, request_handlers);
458}
459
460// When setting activateAfter>0, a pending FetchLater request should be sent
461// after around the specified time, if no navigation happens.
462IN_PROC_BROWSER_TEST_F(FetchLaterActivationTimeoutBrowserTest,
463 SendOnActivationTimeout) {
464 const std::string target_url = kFetchLaterEndpoint;
465 auto request_handlers = RegisterRequestHandlers({target_url});
466 ASSERT_TRUE(server()->Start());
467
468 // Creates a FetchLater request with activateAfter=2s.
469 // It should be sent out after 2s.
470 RunScript(JsReplace(R"(
471 fetchLater($1, {activateAfter: 2000});
472 )",
473 target_url));
474 ASSERT_FALSE(current_document().IsDestroyed());
475
476 // The loader should still exist as the page exists.
477 EXPECT_EQ(loader_service()->NumDisconnectedLoadersForTesting(), 0u);
478 // The FetchLater request should be sent, triggered by its activateAfter.
479 ExpectFetchLaterRequests(1, request_handlers);
480}
481
482// A pending FetchLater request should be sent when its page is evicted out of
483// BackForwardCache.
484IN_PROC_BROWSER_TEST_F(FetchLaterActivationTimeoutBrowserTest,
485 SendOnBackForwardCachedEviction) {
486 const std::string target_url = kFetchLaterEndpoint;
487 auto request_handlers = RegisterRequestHandlers({target_url});
488 ASSERT_TRUE(server()->Start());
489
490 // Creates a FetchLater request with long activateAfter (3min)
491 RunScriptAndNavigateAway(JsReplace(R"(
492 fetchLater($1, {activateAfter: 180000});
493 )",
494 target_url));
495 ASSERT_TRUE(previous_document()->IsInBackForwardCache());
496 // Forces evicting previous page. This will also post a task that destroys it.
497 DisableBFCacheForRFHForTesting(previous_document()->GetGlobalId());
498 ASSERT_TRUE(previous_document()->is_evicted_from_back_forward_cache());
499 // Eviction happens immediately, but RFH deletion may be delayed.
500 ASSERT_TRUE(previous_document().WaitUntilRenderFrameDeleted());
501
502 // The loader is disconnected after the page is evicted by browser process to
503 // start loading the request. However, it may happen earlier or later, so it's
504 // difficult to assert the existence of the disconnected loader.
505
506 // At the end, the FetchLater request should be sent, and the loader is
507 // expected to process the response.
508 ExpectFetchLaterRequests(1, request_handlers);
509}
510
511// All other send-on-BFCache behaviors are covered in
512// send-on-deactivate.tentative.https.window.js
513
514} // namespace content