| // Copyright 2018 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/back_forward_cache_browsertest.h" |
| |
| #include <climits> |
| #include <optional> |
| #include <string_view> |
| #include <unordered_map> |
| |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/location.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/metrics_hashes.h" |
| #include "base/run_loop.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/common/task_annotator.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/test_timeouts.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_log.h" |
| #include "build/build_config.h" |
| #include "build/chromecast_buildflags.h" |
| #include "components/ukm/test_ukm_recorder.h" |
| #include "content/browser/bad_message.h" |
| #include "content/browser/renderer_host/back_forward_cache_can_store_document_result.h" |
| #include "content/browser/renderer_host/back_forward_cache_disable.h" |
| #include "content/browser/renderer_host/back_forward_cache_impl.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/renderer_host/should_swap_browsing_instance.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/common/content_navigation_policy.h" |
| #include "content/common/features.h" |
| #include "content/public/browser/back_forward_cache.h" |
| #include "content/public/browser/document_service.h" |
| #include "content/public/browser/global_routing_id.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/result_codes.h" |
| #include "content/public/test/back_forward_cache_util.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/commit_message_delayer.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/fenced_frame_test_util.h" |
| #include "content/public/test/navigation_handle_observer.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "content/public/test/test_navigation_throttle.h" |
| #include "content/public/test/test_navigation_throttle_inserter.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/public/test/text_input_test_utils.h" |
| #include "content/public/test/url_loader_interceptor.h" |
| #include "content/public/test/web_contents_observer_test_utils.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/shell/browser/shell_javascript_dialog_manager.h" |
| #include "content/test/content_browser_test_utils_internal.h" |
| #include "media/base/media_switches.h" |
| #include "mojo/public/cpp/bindings/message.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "net/base/filename_util.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/controllable_http_response.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "services/metrics/public/cpp/ukm_recorder.h" |
| #include "services/metrics/public/cpp/ukm_source_id.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/service_manager/public/cpp/interface_provider.h" |
| #include "third_party/blink/public/common/device_memory/approximated_device_memory.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/features_generated.h" |
| #include "third_party/blink/public/common/scheduler/web_scheduler_tracked_feature.h" |
| #include "third_party/blink/public/common/switches.h" |
| #include "third_party/blink/public/mojom/back_forward_cache_not_restored_reasons.mojom.h" |
| #include "third_party/blink/public/mojom/frame/back_forward_cache_controller.mojom.h" |
| #include "third_party/blink/public/mojom/frame/frame.mojom.h" |
| #include "third_party/blink/public/mojom/render_accessibility.mojom.h" |
| #include "third_party/blink/public/mojom/script_source_location.mojom.h" |
| |
| // This file has too many tests. |
| // |
| // Before adding new tests to this file, consider if they will fit better into |
| // one of the other back_forward_cache_*_browsertest.cc files or if there are |
| // enough new tests to justify a new file. |
| |
| using testing::_; |
| using testing::Each; |
| using testing::ElementsAre; |
| using testing::Not; |
| using testing::UnorderedElementsAreArray; |
| |
| namespace content { |
| |
| using NotRestoredReasons = |
| BackForwardCacheCanStoreDocumentResult::NotRestoredReasons; |
| using NotRestoredReason = BackForwardCacheMetrics::NotRestoredReason; |
| |
| namespace { |
| |
| class DOMContentLoadedObserver : public WebContentsObserver { |
| public: |
| explicit DOMContentLoadedObserver(RenderFrameHostImpl* render_frame_host) |
| : WebContentsObserver( |
| WebContents::FromRenderFrameHost(render_frame_host)), |
| render_frame_host_(render_frame_host) {} |
| |
| void DOMContentLoaded(RenderFrameHost* render_frame_host) override { |
| if (render_frame_host_ == render_frame_host) |
| run_loop_.Quit(); |
| } |
| |
| [[nodiscard]] bool Wait() { |
| if (render_frame_host_->IsDOMContentLoaded()) |
| run_loop_.Quit(); |
| run_loop_.Run(); |
| return render_frame_host_->IsDOMContentLoaded(); |
| } |
| |
| private: |
| raw_ptr<RenderFrameHostImpl> render_frame_host_; |
| base::RunLoop run_loop_; |
| }; |
| |
| } // namespace |
| |
| bool WaitForDOMContentLoaded(RenderFrameHostImpl* rfh) { |
| DOMContentLoadedObserver observer(rfh); |
| return observer.Wait(); |
| } |
| |
| EvalJsResult GetLocalStorage(RenderFrameHostImpl* rfh, std::string key) { |
| return EvalJs(rfh, JsReplace("localStorage.getItem($1)", key)); |
| } |
| |
| [[nodiscard]] bool WaitForLocalStorage(RenderFrameHostImpl* rfh, |
| std::string key, |
| std::string expected_value) { |
| auto value = EvalJs(rfh, JsReplace(R"( |
| new Promise((resolve) => { |
| let key = $1; |
| let expected_value = $2; |
| if (localStorage.getItem(key) == expected_value) { |
| resolve(localStorage.getItem(key)); |
| return; |
| } |
| let listener = window.addEventListener("storage", e => { |
| if (e.storageArea == localStorage && e.key == key |
| && e.newValue == expected_value) { |
| resolve(localStorage.getItem(key)); |
| removeEventListener("storage", listener); |
| return; |
| } |
| }); |
| }); |
| )", |
| key, expected_value)); |
| return value == expected_value; |
| } |
| |
| BackForwardCacheBrowserTest::BackForwardCacheBrowserTest() = default; |
| |
| BackForwardCacheBrowserTest::~BackForwardCacheBrowserTest() { |
| if (fail_for_unexpected_messages_while_cached_) { |
| // If this is triggered, see MojoInterfaceName in |
| // tools/metrics/histograms/metadata/navigation/enums.xml for which values |
| // correspond which messages. |
| std::vector<base::Bucket> samples = histogram_tester().GetAllSamples( |
| "BackForwardCache.UnexpectedRendererToBrowserMessage." |
| "InterfaceName"); |
| // TODO(crbug.com/40244391): Remove this. |
| // This bucket corresponds to the LocalFrameHost interface. It is known to |
| // be flaky due calls to `LocalFrameHost::DidFocusFrame()` after entering |
| // BFCache. So we ignore it for now by removing it if it's present until we |
| // can fix the root cause. |
| // TODO(crbug.com/40925798): Remove this. |
| // As above but `LocalMainFrameHost::DidFirstVisuallyNonEmptyPaint()`. |
| std::erase_if(samples, [](base::Bucket bucket) { |
| return bucket.min == |
| static_cast<base::HistogramBase::Sample32>(base::HashMetricName( |
| blink::mojom::LocalFrameHost::Name_)) || |
| bucket.min == |
| static_cast<base::HistogramBase::Sample32>(base::HashMetricName( |
| blink::mojom::LocalMainFrameHost::Name_)); |
| }); |
| |
| EXPECT_THAT(samples, testing::ElementsAre()); |
| } |
| } |
| |
| void BackForwardCacheBrowserTest::NotifyNotRestoredReasons( |
| std::unique_ptr<BackForwardCacheCanStoreTreeResult> tree_result) { |
| tree_result_ = std::move(tree_result); |
| } |
| |
| void BackForwardCacheBrowserTest::SetUpCommandLine( |
| base::CommandLine* command_line) { |
| mock_cert_verifier_.SetUpCommandLine(command_line); |
| |
| command_line->AppendSwitch(switches::kUseFakeUIForMediaStream); |
| command_line->AppendSwitch(switches::kEnableExperimentalWebPlatformFeatures); |
| // TODO(sreejakshetty): Initialize ScopedFeatureLists from test constructor. |
| EnableFeatureAndSetParams(features::kBackForwardCacheTimeToLiveControl, |
| "time_to_live_seconds", "3600"); |
| // Entry to the cache can be slow during testing and cause flakiness. |
| DisableFeature(features::kBackForwardCacheEntryTimeout); |
| EnableFeatureAndSetParams(features::kBackForwardCache, |
| "message_handling_when_cached", "log"); |
| EnableFeatureAndSetParams( |
| blink::features::kLogUnexpectedIPCPostedToBackForwardCachedDocuments, |
| "delay_before_tracking_ms", "0"); |
| // Allow unlimited network during tests. Override this if you want to test the |
| // network limiting. |
| EnableFeatureAndSetParams(blink::features::kLoadingTasksUnfreezable, |
| "max_buffered_bytes_per_process", |
| base::NumberToString(INT_MAX)); |
| EnableFeatureAndSetParams(blink::features::kLoadingTasksUnfreezable, |
| "grace_period_to_finish_loading_in_seconds", |
| base::NumberToString(INT_MAX)); |
| // Enable capturing not-restored-reasons tree. |
| EnableFeatureAndSetParams( |
| blink::features::kBackForwardCacheSendNotRestoredReasons, "", ""); |
| |
| // Do not trigger DumpWithoutCrashing() for JavaScript execution. |
| DisableFeature(blink::features::kBackForwardCacheDWCOnJavaScriptExecution); |
| #if BUILDFLAG(IS_ANDROID) |
| EnableFeatureAndSetParams(features::kBackForwardCache, |
| "process_binding_strength", "NORMAL"); |
| #endif |
| // Allow BackForwardCache for all devices regardless of their memory. |
| DisableFeature(features::kBackForwardCacheMemoryControls); |
| // Disables BackForwardCache cache size overwritten by |
| // `content::kBackForwardCacheSize`, as many browser tests here assume |
| // specific or smaller cache size (e.g. 1) rather than 6. |
| DisableFeature(kBackForwardCacheSize); |
| |
| SetupFeaturesAndParameters(); |
| |
| command_line->AppendSwitchASCII( |
| switches::kAutoplayPolicy, |
| switches::autoplay::kNoUserGestureRequiredPolicy); |
| // Unfortunately needed for one test on slow bots, TextInputStateUpdated, |
| // where deferred commits delays input too much. |
| command_line->AppendSwitch(blink::switches::kAllowPreCommitInput); |
| } |
| |
| void BackForwardCacheBrowserTest::SetUpInProcessBrowserTestFixture() { |
| ContentBrowserTest::SetUpInProcessBrowserTestFixture(); |
| mock_cert_verifier_.SetUpInProcessBrowserTestFixture(); |
| } |
| |
| void BackForwardCacheBrowserTest::TearDownInProcessBrowserTestFixture() { |
| ContentBrowserTest::TearDownInProcessBrowserTestFixture(); |
| mock_cert_verifier_.TearDownInProcessBrowserTestFixture(); |
| } |
| |
| void BackForwardCacheBrowserTest::SetupFeaturesAndParameters() { |
| std::vector<base::test::FeatureRefAndParams> enabled_features; |
| |
| for (const auto& [feature_ref, params] : features_with_params_) { |
| enabled_features.emplace_back(*feature_ref, params); |
| } |
| |
| feature_list_.InitWithFeaturesAndParameters(enabled_features, |
| disabled_features_); |
| vmodule_switches_.InitWithSwitches("back_forward_cache_impl=1"); |
| } |
| |
| void BackForwardCacheBrowserTest::EnableFeatureAndSetParams( |
| const base::Feature& feature, |
| std::string param_name, |
| std::string param_value) { |
| features_with_params_[feature][param_name] = param_value; |
| } |
| |
| void BackForwardCacheBrowserTest::DisableFeature(const base::Feature& feature) { |
| disabled_features_.push_back(feature); |
| } |
| |
| void BackForwardCacheBrowserTest::SetUpOnMainThread() { |
| mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| // TestAutoSetUkmRecorder's constructor requires a sequenced context. |
| ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>(); |
| histogram_tester_ = std::make_unique<base::HistogramTester>(); |
| ContentBrowserTest::SetUpOnMainThread(); |
| } |
| |
| void BackForwardCacheBrowserTest::TearDownOnMainThread() { |
| ukm_recorder_.reset(); |
| ContentBrowserTest::TearDownOnMainThread(); |
| } |
| |
| WebContentsImpl* BackForwardCacheBrowserTest::web_contents() const { |
| return static_cast<WebContentsImpl*>(shell()->web_contents()); |
| } |
| |
| RenderFrameHostImpl* BackForwardCacheBrowserTest::current_frame_host() { |
| return web_contents()->GetPrimaryFrameTree().root()->current_frame_host(); |
| } |
| |
| RenderFrameHostManager* |
| BackForwardCacheBrowserTest::render_frame_host_manager() { |
| return web_contents()->GetPrimaryFrameTree().root()->render_manager(); |
| } |
| |
| std::string BackForwardCacheBrowserTest::DepictFrameTree(FrameTreeNode* node) { |
| return visualizer_.DepictFrameTree(node); |
| } |
| |
| bool BackForwardCacheBrowserTest::HistogramContainsIntValue( |
| base::HistogramBase::Sample32 sample, |
| std::vector<base::Bucket> histogram_values) { |
| return base::Contains(histogram_values, static_cast<int>(sample), |
| &base::Bucket::min); |
| } |
| |
| void BackForwardCacheBrowserTest::EvictByJavaScript(RenderFrameHostImpl* rfh) { |
| // Run JavaScript on a page in the back-forward cache. The page should be |
| // evicted. As the frame is deleted, ExecJs returns false without executing. |
| // Run without user gesture to prevent UpdateUserActivationState message |
| // being sent back to browser. |
| EXPECT_FALSE( |
| ExecJs(rfh, "console.log('hi');", EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| } |
| |
| void BackForwardCacheBrowserTest::StartRecordingEvents( |
| RenderFrameHostImpl* rfh) { |
| EXPECT_TRUE(ExecJs(rfh, R"( |
| window.testObservedEvents = []; |
| let event_list = [ |
| 'visibilitychange', |
| 'pagehide', |
| 'pageshow', |
| 'freeze', |
| 'resume', |
| ]; |
| for (event_name of event_list) { |
| let result = event_name; |
| window.addEventListener(event_name, event => { |
| if (event.persisted) |
| result += '.persisted'; |
| window.testObservedEvents.push('window.' + result); |
| }); |
| document.addEventListener(event_name, |
| () => window.testObservedEvents.push('document.' + result)); |
| } |
| )")); |
| } |
| |
| void BackForwardCacheBrowserTest::MatchEventList(RenderFrameHostImpl* rfh, |
| base::Value list, |
| base::Location location) { |
| EXPECT_EQ(list, EvalJs(rfh, "window.testObservedEvents")) |
| << location.ToString(); |
| } |
| |
| // Creates a minimal HTTPS server, accessible through https_server(). |
| // Returns a pointer to the server. |
| net::EmbeddedTestServer* BackForwardCacheBrowserTest::CreateHttpsServer() { |
| https_server_ = std::make_unique<net::EmbeddedTestServer>( |
| net::EmbeddedTestServer::TYPE_HTTPS); |
| https_server_->AddDefaultHandlers(GetTestDataFilePath()); |
| https_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES); |
| return https_server(); |
| } |
| |
| net::EmbeddedTestServer* BackForwardCacheBrowserTest::https_server() { |
| return https_server_.get(); |
| } |
| |
| // Do not fail this test if a message from a renderer arrives at the browser |
| // for a cached page. |
| void BackForwardCacheBrowserTest::DoNotFailForUnexpectedMessagesWhileCached() { |
| fail_for_unexpected_messages_while_cached_ = false; |
| } |
| |
| // Navigates to a page at |page_url| with an img element with src set to |
| // "image.png". |
| RenderFrameHostImpl* BackForwardCacheBrowserTest::NavigateToPageWithImage( |
| const GURL& page_url) { |
| EXPECT_TRUE(NavigateToURL(shell(), page_url)); |
| RenderFrameHostImpl* rfh = current_frame_host(); |
| // Wait for the document to load DOM to ensure that kLoading is not |
| // one of the reasons why the document wasn't cached. |
| EXPECT_TRUE(WaitForDOMContentLoaded(rfh)); |
| |
| EXPECT_TRUE(ExecJs(rfh, R"( |
| var image = document.createElement("img"); |
| image.src = "image.png"; |
| document.body.appendChild(image); |
| |
| var image_load_status = new Promise((resolve, reject) => { |
| image.onload = () => { resolve("loaded"); } |
| image.onerror = () => { resolve("error"); } |
| }); |
| )")); |
| return rfh; |
| } |
| |
| void BackForwardCacheBrowserTest::AcquireKeyboardLock( |
| RenderFrameHostImpl* rfh) { |
| EXPECT_EQ(42, EvalJs(rfh, R"( |
| new Promise(resolve => { |
| navigator.keyboard.lock(); |
| resolve(42); |
| }); |
| )")); |
| } |
| |
| void BackForwardCacheBrowserTest::ReleaseKeyboardLock( |
| RenderFrameHostImpl* rfh) { |
| EXPECT_EQ(42, EvalJs(rfh, R"( |
| new Promise(resolve => { |
| navigator.keyboard.unlock(); |
| resolve(42); |
| }); |
| )")); |
| } |
| |
| void BackForwardCacheBrowserTest::NavigateAndBlock(GURL url, |
| int history_offset) { |
| // Block the navigation with an error. |
| std::unique_ptr<URLLoaderInterceptor> url_interceptor = |
| URLLoaderInterceptor::SetupRequestFailForURL(url, |
| net::ERR_BLOCKED_BY_CLIENT); |
| if (history_offset) { |
| shell()->GoBackOrForward(history_offset); |
| } else { |
| shell()->LoadURL(url); |
| } |
| WaitForLoadStop(web_contents()); |
| ASSERT_EQ(current_frame_host()->GetLastCommittedURL(), url); |
| ASSERT_TRUE(current_frame_host()->IsErrorDocument()); |
| } |
| |
| ReasonsMatcher BackForwardCacheBrowserTest::MatchesNotRestoredReasons( |
| const std::optional<testing::Matcher<std::string>>& id, |
| const std::optional<testing::Matcher<std::string>>& name, |
| const std::optional<testing::Matcher<std::string>>& src, |
| const std::vector<BlockingDetailsReasonsMatcher>& reasons, |
| const std::optional<SameOriginMatcher>& same_origin_details) { |
| // TODO(crbug.com/41496143) Make this matcher display human-friendly messages. |
| return testing::Pointee(testing::AllOf( |
| id.has_value() |
| ? testing::Field( |
| "id", &blink::mojom::BackForwardCacheNotRestoredReasons::id, |
| testing::Optional(id.value())) |
| : testing::Field( |
| "id", &blink::mojom::BackForwardCacheNotRestoredReasons::id, |
| std::optional<std::string>(std::nullopt)), |
| name.has_value() |
| ? testing::Field( |
| "name", &blink::mojom::BackForwardCacheNotRestoredReasons::name, |
| testing::Optional(name.value())) |
| : testing::Field( |
| "name", &blink::mojom::BackForwardCacheNotRestoredReasons::name, |
| std::optional<std::string>(std::nullopt)), |
| src.has_value() |
| ? testing::Field( |
| "src", &blink::mojom::BackForwardCacheNotRestoredReasons::src, |
| testing::Optional(src.value())) |
| : testing::Field( |
| "src", &blink::mojom::BackForwardCacheNotRestoredReasons::src, |
| std::optional<std::string>(std::nullopt)), |
| testing::Field("reasons", |
| &blink::mojom::BackForwardCacheNotRestoredReasons::reasons, |
| testing::UnorderedElementsAreArray(reasons)), |
| testing::Field( |
| "same_origin_details", |
| &blink::mojom::BackForwardCacheNotRestoredReasons:: |
| same_origin_details, |
| same_origin_details.has_value() |
| ? same_origin_details.value() |
| : testing::Property( |
| "is_null", |
| &blink::mojom::SameOriginBfcacheNotRestoredDetailsPtr:: |
| is_null, |
| true)))); |
| } |
| |
| SameOriginMatcher BackForwardCacheBrowserTest::MatchesSameOriginDetails( |
| const testing::Matcher<GURL>& url, |
| const std::vector<ReasonsMatcher>& children) { |
| // TODO(crbug.com/41496143) Make this matcher display human-friendly messages. |
| return testing::Pointee(testing::AllOf( |
| testing::Field( |
| "url", &blink::mojom::SameOriginBfcacheNotRestoredDetails::url, url), |
| testing::Field( |
| "children", |
| &blink::mojom::SameOriginBfcacheNotRestoredDetails::children, |
| testing::ElementsAreArray(children)))); |
| } |
| |
| BlockingDetailsReasonsMatcher |
| BackForwardCacheBrowserTest::MatchesDetailedReason( |
| const testing::Matcher<std::string>& name, |
| const std::optional<SourceLocationMatcher>& source) { |
| // TODO(crbug.com/41496143) Make this matcher display human-friendly |
| // messages. |
| return testing::Pointee(testing::AllOf( |
| testing::Field("name", &blink::mojom::BFCacheBlockingDetailedReason::name, |
| name), |
| testing::Field( |
| "source", &blink::mojom::BFCacheBlockingDetailedReason::source, |
| source.has_value() |
| ? source.value() |
| : testing::Property( |
| "is_null", &blink::mojom::ScriptSourceLocationPtr::is_null, |
| true)))); |
| } |
| |
| BlockingDetailsMatcher BackForwardCacheBrowserTest::MatchesBlockingDetails( |
| const std::optional<SourceLocationMatcher>& source) { |
| // TODO(crbug.com/41496143) Make this matcher display human-friendly messages. |
| return testing::Pointee(testing::Field( |
| "source", &blink::mojom::BlockingDetails::source, |
| source.has_value() |
| ? source.value() |
| : testing::Property("is_null", |
| &blink::mojom::ScriptSourceLocationPtr::is_null, |
| true))); |
| } |
| |
| SourceLocationMatcher BackForwardCacheBrowserTest::MatchesSourceLocation( |
| const testing::Matcher<GURL>& url, |
| const testing::Matcher<std::string>& function_name, |
| const testing::Matcher<uint64_t>& line_number, |
| const testing::Matcher<uint64_t>& column_number) { |
| // TODO(crbug.com/41496143) Make this matcher display human-friendly |
| // messages. |
| return testing::Pointee(testing::AllOf( |
| testing::Field("url", &blink::mojom::ScriptSourceLocation::url, url), |
| testing::Field("function_name", |
| &blink::mojom::ScriptSourceLocation::function_name, |
| function_name), |
| testing::Field("line_number", |
| &blink::mojom::ScriptSourceLocation::line_number, |
| line_number), |
| testing::Field("column_number", |
| &blink::mojom::ScriptSourceLocation::column_number, |
| column_number))); |
| } |
| |
| void BackForwardCacheUnloadBrowserTest::SetUpCommandLine( |
| base::CommandLine* command_line) { |
| BackForwardCacheBrowserTest::SetUpCommandLine(command_line); |
| scoped_feature_list_.InitAndEnableFeature(kBackForwardCacheUnloadAllowed); |
| } |
| |
| std::initializer_list<RenderFrameHostImpl*> Elements( |
| std::initializer_list<RenderFrameHostImpl*> t) { |
| return t; |
| } |
| |
| // Execute a custom callback when navigation is ready to commit. This is |
| // useful for simulating race conditions happening when a page enters the |
| // BackForwardCache and receive inflight messages sent when it wasn't frozen |
| // yet. |
| class ReadyToCommitNavigationCallback : public WebContentsObserver { |
| public: |
| ReadyToCommitNavigationCallback( |
| WebContents* content, |
| base::OnceCallback<void(NavigationHandle*)> callback) |
| : WebContentsObserver(content), callback_(std::move(callback)) {} |
| |
| ReadyToCommitNavigationCallback(const ReadyToCommitNavigationCallback&) = |
| delete; |
| ReadyToCommitNavigationCallback& operator=( |
| const ReadyToCommitNavigationCallback&) = delete; |
| |
| private: |
| // WebContentsObserver: |
| void ReadyToCommitNavigation(NavigationHandle* navigation_handle) override { |
| if (callback_) |
| std::move(callback_).Run(navigation_handle); |
| } |
| |
| base::OnceCallback<void(NavigationHandle*)> callback_; |
| }; |
| |
| class FirstVisuallyNonEmptyPaintObserver : public WebContentsObserver { |
| public: |
| explicit FirstVisuallyNonEmptyPaintObserver(WebContents* contents) |
| : WebContentsObserver(contents) {} |
| void DidFirstVisuallyNonEmptyPaint() override { |
| if (observed_) |
| return; |
| observed_ = true; |
| run_loop_.Quit(); |
| } |
| |
| bool did_fire() const { return observed_; } |
| |
| void Wait() { run_loop_.Run(); } |
| |
| private: |
| bool observed_ = false; |
| base::RunLoop run_loop_{base::RunLoop::Type::kNestableTasksAllowed}; |
| }; |
| |
| void WaitForFirstVisuallyNonEmptyPaint(WebContents* contents) { |
| if (contents->CompletedFirstVisuallyNonEmptyPaint()) |
| return; |
| FirstVisuallyNonEmptyPaintObserver observer(contents); |
| observer.Wait(); |
| } |
| |
| class ThemeColorObserver : public WebContentsObserver { |
| public: |
| explicit ThemeColorObserver(WebContents* contents) |
| : WebContentsObserver(contents) {} |
| |
| // Can only be called once. |
| [[nodiscard]] bool WaitUntilThemeColorChange() { |
| CHECK(!loop_); |
| loop_ = std::make_unique<base::RunLoop>(); |
| if (observed_) { |
| return true; |
| } |
| loop_->Run(); |
| return observed_; |
| } |
| |
| void DidChangeThemeColor() override { |
| observed_ = true; |
| if (loop_) { |
| loop_->Quit(); |
| } |
| } |
| |
| bool did_fire() const { return observed_; } |
| |
| private: |
| std::unique_ptr<base::RunLoop> loop_; |
| bool observed_ = false; |
| }; |
| |
| PageLifecycleStateManagerTestDelegate::PageLifecycleStateManagerTestDelegate( |
| PageLifecycleStateManager* manager) |
| : manager_(manager) { |
| manager->SetDelegateForTesting(this); |
| } |
| |
| PageLifecycleStateManagerTestDelegate:: |
| ~PageLifecycleStateManagerTestDelegate() { |
| if (manager_) |
| manager_->SetDelegateForTesting(nullptr); |
| } |
| |
| bool PageLifecycleStateManagerTestDelegate::WaitForInBackForwardCacheAck() { |
| DCHECK(manager_); |
| if (manager_->last_acknowledged_state().is_in_back_forward_cache) { |
| return true; |
| } |
| base::RunLoop loop; |
| store_in_back_forward_cache_ack_received_ = loop.QuitClosure(); |
| loop.Run(); |
| return manager_->last_acknowledged_state().is_in_back_forward_cache; |
| } |
| |
| void PageLifecycleStateManagerTestDelegate::OnStoreInBackForwardCacheSent( |
| base::OnceClosure cb) { |
| store_in_back_forward_cache_sent_ = std::move(cb); |
| } |
| |
| void PageLifecycleStateManagerTestDelegate::OnDisableJsEvictionSent( |
| base::OnceClosure cb) { |
| disable_eviction_sent_ = std::move(cb); |
| } |
| |
| void PageLifecycleStateManagerTestDelegate::OnRestoreFromBackForwardCacheSent( |
| base::OnceClosure cb) { |
| restore_from_back_forward_cache_sent_ = std::move(cb); |
| } |
| |
| void PageLifecycleStateManagerTestDelegate::OnLastAcknowledgedStateChanged( |
| const blink::mojom::PageLifecycleState& old_state, |
| const blink::mojom::PageLifecycleState& new_state) { |
| if (store_in_back_forward_cache_ack_received_ && |
| new_state.is_in_back_forward_cache) |
| std::move(store_in_back_forward_cache_ack_received_).Run(); |
| } |
| |
| void PageLifecycleStateManagerTestDelegate::OnUpdateSentToRenderer( |
| const blink::mojom::PageLifecycleState& new_state) { |
| if (store_in_back_forward_cache_sent_ && new_state.is_in_back_forward_cache) { |
| std::move(store_in_back_forward_cache_sent_).Run(); |
| } |
| |
| if (disable_eviction_sent_ && new_state.eviction_enabled == false) { |
| std::move(disable_eviction_sent_).Run(); |
| } |
| |
| if (restore_from_back_forward_cache_sent_ && |
| !new_state.is_in_back_forward_cache) { |
| std::move(restore_from_back_forward_cache_sent_).Run(); |
| } |
| } |
| |
| void PageLifecycleStateManagerTestDelegate::OnDeleted() { |
| manager_ = nullptr; |
| } |
| |
| // Check the visible URL in the omnibox is properly updated when restoring a |
| // document from the BackForwardCache. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, VisibleURL) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // 1) Go to A. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| |
| // 2) Go to B. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| |
| // 3) Go back to A. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| EXPECT_EQ(url_a, web_contents()->GetVisibleURL()); |
| |
| // 4) Go forward to B. |
| ASSERT_TRUE(HistoryGoForward(web_contents())); |
| EXPECT_EQ(url_b, web_contents()->GetVisibleURL()); |
| } |
| |
| // Test only 1 document is kept in the at a time BackForwardCache. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| CacheSizeLimitedToOneDocumentPerTab) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| GURL url_c(embedded_test_server()->GetURL("c.com", "/title1.html")); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| // BackForwardCache is empty. |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| // BackForwardCache contains only rfh_a. |
| RenderFrameHostImpl* rfh_b = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_b(rfh_b); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url_c)); |
| // BackForwardCache contains only rfh_b. |
| delete_observer_rfh_a.WaitUntilDeleted(); |
| EXPECT_FALSE(delete_observer_rfh_b.deleted()); |
| |
| // If/when the cache size is increased, this can be tested iteratively, see |
| // deleted code in: https://crrev.com/c/1782902. |
| |
| ASSERT_TRUE(HistoryGoToOffset(web_contents(), -2)); |
| ExpectNotRestored({BackForwardCacheMetrics::NotRestoredReason::kCacheLimit}, |
| {}, {}, {}, {}, FROM_HERE); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, ResponseHeaders) { |
| CreateHttpsServer(); |
| ASSERT_TRUE(https_server()->Start()); |
| |
| GURL url_a(https_server()->GetURL("a.test", "/set-header?X-Foo: bar")); |
| GURL url_b(https_server()->GetURL("b.test", "/title1.html")); |
| |
| // 1) Navigate to A. |
| NavigationHandleObserver observer1(web_contents(), url_a); |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a); |
| EXPECT_TRUE(observer1.has_committed()); |
| EXPECT_EQ("bar", observer1.GetNormalizedResponseHeader("x-foo")); |
| |
| // 2) Navigate to B. |
| NavigationHandleObserver observer2(web_contents(), url_b); |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| RenderFrameHostImpl* rfh_b = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_b(rfh_b); |
| EXPECT_FALSE(delete_observer_rfh_a.deleted()); |
| EXPECT_TRUE(rfh_a->IsInBackForwardCache()); |
| EXPECT_FALSE(rfh_b->IsInBackForwardCache()); |
| EXPECT_TRUE(observer2.has_committed()); |
| |
| // 3) Go back to A. |
| NavigationHandleObserver observer3(web_contents(), url_a); |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| EXPECT_FALSE(delete_observer_rfh_a.deleted()); |
| EXPECT_FALSE(delete_observer_rfh_b.deleted()); |
| EXPECT_EQ(rfh_a, current_frame_host()); |
| EXPECT_FALSE(rfh_a->IsInBackForwardCache()); |
| EXPECT_TRUE(rfh_b->IsInBackForwardCache()); |
| EXPECT_TRUE(observer3.has_committed()); |
| EXPECT_EQ("bar", observer3.GetNormalizedResponseHeader("x-foo")); |
| |
| ExpectRestored(FROM_HERE); |
| } |
| |
| void HighCacheSizeBackForwardCacheBrowserTest::SetUpCommandLine( |
| base::CommandLine* command_line) { |
| EnableFeatureAndSetParams(features::kBackForwardCache, "cache_size", |
| base::NumberToString(kBackForwardCacheSize)); |
| BackForwardCacheBrowserTest::SetUpCommandLine(command_line); |
| } |
| |
| // Test documents are evicted from the BackForwardCache at some point. |
| IN_PROC_BROWSER_TEST_F(HighCacheSizeBackForwardCacheBrowserTest, |
| CacheEvictionWithIncreasedCacheSize) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); // BackForwardCache size is 0. |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); // BackForwardCache size is 1. |
| RenderFrameHostImpl* rfh_b = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_b(rfh_b); |
| |
| for (size_t i = 2; i < kBackForwardCacheSize; ++i) { |
| EXPECT_TRUE(NavigateToURL(shell(), i % 2 ? url_b : url_a)); |
| // After |i+1| navigations, |i| documents went into the BackForwardCache. |
| // When |i| is greater than the BackForwardCache size limit, they are |
| // evicted: |
| EXPECT_EQ(i >= kBackForwardCacheSize + 1, delete_observer_rfh_a.deleted()); |
| EXPECT_EQ(i >= kBackForwardCacheSize + 2, delete_observer_rfh_b.deleted()); |
| } |
| } |
| |
| // Tests that evicting a page in between the time the back/forward cache |
| // NavigationRequest restore was created and when the NavigationRequest actually |
| // starts after finishing beforeunload won't result in a crash. |
| // See https://crbug.com/1218114. |
| IN_PROC_BROWSER_TEST_F(HighCacheSizeBackForwardCacheBrowserTest, |
| EvictedWhileWaitingForBeforeUnload) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| GURL url_c(embedded_test_server()->GetURL("c.com", "/title3.html")); |
| |
| // 1) Navigate to A. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImplWrapper rfh_a(current_frame_host()); |
| |
| // 2) Navigate to B. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| RenderFrameHostImplWrapper rfh_b(current_frame_host()); |
| EXPECT_TRUE(rfh_a->IsInBackForwardCache()); |
| |
| // 3) Navigate to C, which has a beforeunload handler that never finishes. |
| EXPECT_TRUE(NavigateToURL(shell(), url_c)); |
| RenderFrameHostImplWrapper rfh_c(current_frame_host()); |
| EXPECT_TRUE(ExecJs(rfh_c.get(), R"( |
| window.onbeforeunload = () => { |
| while (true) {} |
| } |
| )")); |
| // Both A & B are in the back/forward cache. |
| EXPECT_TRUE(rfh_a->IsInBackForwardCache()); |
| EXPECT_TRUE(rfh_b->IsInBackForwardCache()); |
| |
| // 4) Evict entry A. This will post a task that destroys all evicted entries |
| // when it runs (task #1). |
| DisableBFCacheForRFHForTesting(rfh_a->GetGlobalId()); |
| EXPECT_FALSE(rfh_a.IsDestroyed()); |
| EXPECT_TRUE(rfh_a->is_evicted_from_back_forward_cache()); |
| |
| // 5) Trigger a back navigation to B. This will create a BFCache restore |
| // navigation to B, but will wait for C's beforeunload handler to finish |
| // running before continuing. |
| // The BFCache entry will be evicted before the back navigation completes, so |
| // the old navigation will be reset and a new navigation will be restarted. |
| // This observer is waiting for the two navigation requests to complete. |
| TestNavigationObserver observer(web_contents(), |
| /* expected_number_of_navigations= */ 2, |
| MessageLoopRunner::QuitMode::IMMEDIATE, |
| /* ignore_uncommitted_navigations= */ false); |
| web_contents()->GetController().GoBack(); |
| |
| // 6) Post a task to run BeforeUnloadCompleted (task #2). This will continue |
| // the BFCache restore navigation to B from step 5, which is currently waiting |
| // for a BeforeUnloadCompleted call. |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindLambdaForTesting([&]() { |
| root->navigator().BeforeUnloadCompleted( |
| root, /*proceed=*/true, base::TimeTicks::Now(), |
| /*for_legacy=*/false, /*showed_dialog=*/false); |
| })); |
| |
| // 7) Evict entry B. This will post a task (task #3) to restart the navigation |
| // to B, and also another task (task #4) to destroy all evicted entries. |
| DisableBFCacheForRFHForTesting(rfh_b->GetGlobalId()); |
| EXPECT_FALSE(rfh_b.IsDestroyed()); |
| EXPECT_TRUE(rfh_b->is_evicted_from_back_forward_cache()); |
| |
| // 8) Wait until the back navigation to B finishes. This will run posted tasks |
| // in order. So: |
| // - Task #1 from step 4 will run and destroy all evicted entries. As both the |
| // entries for A & B have been evicted, they are both destroyed. |
| // - Task #2 from step 6 will run and continue the back/forward cache restore |
| // NavigationRequest to B. However, it would notice that the entry for B is |
| // now gone, and should handle it gracefully. |
| // - Task #3 from step 7 to restart navigation to B runs, and should create a |
| // NavigationRequest to replace the previous NavigationRequest to B. |
| // - Task #4 from step 7 to destroy evicted entries runs and won't destroy |
| // any entry since there's no longer any entry in the back/forward cache. |
| observer.Wait(); |
| EXPECT_EQ(web_contents()->GetLastCommittedURL(), url_b); |
| ExpectNotRestored({BackForwardCacheMetrics::NotRestoredReason:: |
| kDisableForRenderFrameHostCalled}, |
| {}, {}, {RenderFrameHostDisabledForTestingReason()}, {}, |
| FROM_HERE); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| SubframeWithOngoingNavigationNotCached) { |
| net::test_server::ControllableHttpResponse response(embedded_test_server(), |
| "/hung"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // Navigate to a page with an iframe. |
| TestNavigationObserver navigation_observer1(web_contents()); |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/back_forward_cache/page_with_hung_iframe.html")); |
| shell()->LoadURL(main_url); |
| navigation_observer1.WaitForNavigationFinished(); |
| |
| RenderFrameHostImpl* main_frame = current_frame_host(); |
| RenderFrameDeletedObserver frame_deleted_observer(main_frame); |
| response.WaitForRequest(); |
| |
| // Navigate away. |
| TestNavigationObserver navigation_observer2(web_contents()); |
| shell()->LoadURL(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| navigation_observer2.WaitForNavigationFinished(); |
| |
| // The page with the unsupported feature should be deleted (not cached). |
| frame_deleted_observer.WaitUntilDeleted(); |
| } |
| |
| // Only HTTP/HTTPS main document can enter the BackForwardCache. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, CacheHTTPDocumentOnly) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| ASSERT_TRUE(CreateHttpsServer()->Start()); |
| |
| GURL http_url(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| GURL https_url(https_server()->GetURL("a.test", "/title1.html")); |
| GURL file_url = net::FilePathToFileURL(GetTestFilePath("", "title1.html")); |
| GURL data_url = GURL("data:text/html,"); |
| GURL blank_url = GURL(url::kAboutBlankURL); |
| GURL webui_url = GetWebUIURL("gpu"); |
| |
| enum { STORED, DELETED }; |
| struct { |
| int expectation; |
| GURL url; |
| } test_cases[] = { |
| // Only document with HTTP/HTTPS URLs are allowed to enter the |
| // BackForwardCache. |
| {STORED, http_url}, |
| {STORED, https_url}, |
| |
| // Others aren't allowed. |
| {DELETED, file_url}, |
| {DELETED, data_url}, |
| {DELETED, webui_url}, |
| {DELETED, blank_url}, |
| }; |
| |
| char hostname[] = "a.unique"; |
| for (auto& test_case : test_cases) { |
| SCOPED_TRACE(testing::Message() |
| << std::endl |
| << "expectation = " << test_case.expectation << std::endl |
| << "url = " << test_case.url << std::endl); |
| |
| // 1) Navigate to. |
| EXPECT_TRUE(NavigateToURL(shell(), test_case.url)); |
| RenderFrameHostImplWrapper rfh(current_frame_host()); |
| |
| // 2) Navigate away. |
| hostname[0]++; |
| GURL reset_url(embedded_test_server()->GetURL(hostname, "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), reset_url)); |
| |
| if (test_case.expectation == STORED) { |
| EXPECT_FALSE(rfh.IsRenderFrameDeleted()); |
| EXPECT_TRUE(rfh->IsInBackForwardCache()); |
| continue; |
| } |
| |
| if (rfh.get() == current_frame_host()) { |
| // If the RenderFrameHost is reused, it won't be deleted, so don't wait |
| // for deletion. Just check that it's not saved in the back-forward cache. |
| EXPECT_FALSE(rfh.IsRenderFrameDeleted()); |
| EXPECT_FALSE(rfh->IsInBackForwardCache()); |
| continue; |
| } |
| |
| // When the RenderFrameHost is not reused and it's not stored in the |
| // back-forward cache, it will eventually be deleted. |
| ASSERT_TRUE(rfh.WaitUntilRenderFrameDeleted()); |
| } |
| } |
| |
| // Regression test for https://crbug.com/993337. |
| // |
| // A note about sharing BrowsingInstances and the BackForwardCache: |
| // |
| // We should never keep around more than one main frame that belongs to the same |
| // BrowsingInstance. When swapping two pages, when one is stored in the |
| // back-forward cache or one is restored from it, the current code expects the |
| // two to live in different BrowsingInstances. |
| // |
| // History navigation can recreate a page with the same BrowsingInstance as the |
| // one stored in the back-forward cache. This case must to be handled. When it |
| // happens, the back-forward cache page is evicted. |
| // |
| // Since cache eviction is asynchronous, it's is possible for two main frames |
| // belonging to the same BrowsingInstance to be alive for a brief period of time |
| // (the new page being navigated to, and a page in the cache, until it is |
| // destroyed asynchronously via eviction). |
| // |
| // The test below tests that the brief period of time where two main frames are |
| // alive in the same BrowsingInstance does not cause anything to blow up. |
| |
| // TODO(crbug.com/1127979, crbug.com/1446206): Flaky on Linux, Windows and |
| // ChromeOS, iOS, and Mac. |
| #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS) || \ |
| BUILDFLAG(IS_MAC) || BUILDFLAG(IS_IOS) |
| #define MAYBE_NavigateToTwoPagesOnSameSite DISABLED_NavigateToTwoPagesOnSameSite |
| #else |
| #define MAYBE_NavigateToTwoPagesOnSameSite NavigateToTwoPagesOnSameSite |
| #endif |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| MAYBE_NavigateToTwoPagesOnSameSite) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a1(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_a2(embedded_test_server()->GetURL("a.com", "/title2.html")); |
| GURL url_b3(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // 1) Navigate to A1. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a1)); |
| |
| // 2) Navigate to A2. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a2)); |
| RenderFrameHostImpl* rfh_a2 = current_frame_host(); |
| RenderFrameDeletedObserver delete_rfh_a2(current_frame_host()); |
| |
| // 3) Navigate to B3. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b3)); |
| EXPECT_TRUE(rfh_a2->IsInBackForwardCache()); |
| RenderFrameHostImpl* rfh_b3 = current_frame_host(); |
| |
| // 4) Do a history navigation back to A1. |
| ASSERT_TRUE(HistoryGoToIndex(web_contents(), 0)); |
| EXPECT_TRUE(rfh_b3->IsInBackForwardCache()); |
| |
| // Note that the frame for A1 gets created before A2 is deleted from the |
| // cache, so there will be a brief period where two the main frames (A1 and |
| // A2) are alive in the same BrowsingInstance/SiteInstance, at the same time. |
| // That is the scenario this test is covering. This used to cause a CHECK, |
| // because the two main frames shared a single RenderViewHost (no longer the |
| // case after https://crrev.com/c/1833616). |
| |
| // A2 should be evicted from the cache and asynchronously deleted, due to the |
| // cache size limit (B3 took its place in the cache). |
| delete_rfh_a2.WaitUntilDeleted(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| NavigateToTwoPagesOnSameSiteWithSubframes) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| // This test covers the same scenario as NavigateToTwoPagesOnSameSite, except |
| // the pages contain subframes: |
| // A1(B) -> A2(B(C)) -> D3 -> A1(B) |
| // |
| // The subframes shouldn't make a difference, so the expected behavior is the |
| // same as NavigateToTwoPagesOnSameSite. |
| GURL url_a1(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| GURL url_a2(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b(c))")); |
| GURL url_d3(embedded_test_server()->GetURL("d.com", "/title1.html")); |
| |
| // 1) Navigate to A1(B). |
| EXPECT_TRUE(NavigateToURL(shell(), url_a1)); |
| |
| // 2) Navigate to A2(B(C)). |
| EXPECT_TRUE(NavigateToURL(shell(), url_a2)); |
| RenderFrameHostImpl* rfh_a2 = current_frame_host(); |
| RenderFrameDeletedObserver delete_rfh_a2(current_frame_host()); |
| |
| // 3) Navigate to D3. |
| EXPECT_TRUE(NavigateToURL(shell(), url_d3)); |
| EXPECT_TRUE(rfh_a2->IsInBackForwardCache()); |
| RenderFrameHostImpl* rfh_d3 = current_frame_host(); |
| |
| // 4) Do a history navigation back to A1(B). |
| ASSERT_TRUE(HistoryGoToIndex(web_contents(), 0)); |
| |
| // D3 takes A2(B(C))'s place in the cache. |
| EXPECT_TRUE(rfh_d3->IsInBackForwardCache()); |
| delete_rfh_a2.WaitUntilDeleted(); |
| } |
| |
| // Sub-frame doesn't transition from LifecycleStateImpl::kInBackForwardCache to |
| // LifecycleStateImpl::kRunningUnloadHandlers even when the sub-frame having |
| // unload handlers is being evicted from BackForwardCache. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheUnloadBrowserTest, |
| SubframeWithUnloadHandler) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a.com(a.com)")); |
| GURL child_url = embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a.com()"); |
| GURL url_2(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| |
| // 1) Navigate to |main_url|. |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImpl* main_rfh = current_frame_host(); |
| ASSERT_EQ(1U, main_rfh->child_count()); |
| RenderFrameHostImpl* child_rfh = main_rfh->child_at(0)->current_frame_host(); |
| RenderFrameDeletedObserver main_rfh_observer(main_rfh), |
| child_rfh_observer(child_rfh); |
| |
| // 2) Add an unload handler to the child RFH. |
| EXPECT_TRUE(ExecJs(child_rfh, "window.onunload = () => {} ")); |
| |
| // 3) Navigate to |url_2|. |
| EXPECT_TRUE(NavigateToURL(shell(), url_2)); |
| |
| // 4) The previous main RFH and child RFH should be in the back-forward |
| // cache. |
| EXPECT_FALSE(main_rfh_observer.deleted()); |
| EXPECT_FALSE(child_rfh_observer.deleted()); |
| EXPECT_TRUE(main_rfh->IsInBackForwardCache()); |
| EXPECT_TRUE(child_rfh->IsInBackForwardCache()); |
| |
| // Destruction of bfcached page happens after shutdown and it should not |
| // trigger unload handlers and be destroyed directly. |
| } |
| |
| // Do a same document navigation and make sure we do not fire the |
| // DidFirstVisuallyNonEmptyPaint again |
| IN_PROC_BROWSER_TEST_F( |
| BackForwardCacheBrowserTest, |
| DoesNotFireDidFirstVisuallyNonEmptyPaintForSameDocumentNavigation) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a_1(embedded_test_server()->GetURL( |
| "a.com", "/accessibility/html/a-name.html")); |
| GURL url_a_2(embedded_test_server()->GetURL( |
| "a.com", "/accessibility/html/a-name.html#id")); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), url_a_1)); |
| WaitForFirstVisuallyNonEmptyPaint(shell()->web_contents()); |
| |
| FirstVisuallyNonEmptyPaintObserver observer(web_contents()); |
| EXPECT_TRUE(NavigateToURL(shell(), url_a_2)); |
| // Make sure the bfcache restore code does not fire the event during commit |
| // navigation. |
| EXPECT_FALSE(observer.did_fire()); |
| EXPECT_TRUE(web_contents()->CompletedFirstVisuallyNonEmptyPaint()); |
| } |
| |
| // Make sure we fire DidFirstVisuallyNonEmptyPaint when restoring from bf-cache. |
| // TODO(crbug.com/327195951): Re-enable this test |
| #if BUILDFLAG(IS_ANDROID) |
| #define MAYBE_FiresDidFirstVisuallyNonEmptyPaintWhenRestoredFromCache \ |
| DISABLED_FiresDidFirstVisuallyNonEmptyPaintWhenRestoredFromCache |
| #else |
| #define MAYBE_FiresDidFirstVisuallyNonEmptyPaintWhenRestoredFromCache \ |
| FiresDidFirstVisuallyNonEmptyPaintWhenRestoredFromCache |
| #endif |
| IN_PROC_BROWSER_TEST_F( |
| BackForwardCacheBrowserTest, |
| MAYBE_FiresDidFirstVisuallyNonEmptyPaintWhenRestoredFromCache) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // 1) Navigate to A. |
| ASSERT_TRUE(NavigateToURL(shell(), url_a)); |
| WaitForFirstVisuallyNonEmptyPaint(shell()->web_contents()); |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a); |
| |
| // 2) Navigate to B. |
| ASSERT_TRUE(NavigateToURL(shell(), url_b)); |
| ASSERT_FALSE(delete_observer_rfh_a.deleted()); |
| ASSERT_TRUE(rfh_a->IsInBackForwardCache()); |
| WaitForFirstVisuallyNonEmptyPaint(shell()->web_contents()); |
| |
| // 3) Navigate to back to A. |
| FirstVisuallyNonEmptyPaintObserver observer(web_contents()); |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| // Make sure the bfcache restore code does fire the event during commit |
| // navigation. |
| EXPECT_TRUE(web_contents()->CompletedFirstVisuallyNonEmptyPaint()); |
| EXPECT_TRUE(observer.did_fire()); |
| } |
| #if BUILDFLAG(IS_ANDROID) |
| #define MAYBE_SetsThemeColorWhenRestoredFromCache \ |
| DISABLED_SetsThemeColorWhenRestoredFromCache |
| #else |
| #define MAYBE_SetsThemeColorWhenRestoredFromCache \ |
| SetsThemeColorWhenRestoredFromCache |
| #endif |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| MAYBE_SetsThemeColorWhenRestoredFromCache) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/theme_color.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), url_a)); |
| WaitForFirstVisuallyNonEmptyPaint(web_contents()); |
| RenderFrameHostImplWrapper rfh_a(current_frame_host()); |
| EXPECT_EQ(web_contents()->GetThemeColor(), 0xFFFF0000u); |
| |
| ASSERT_TRUE(NavigateToURL(shell(), url_b)); |
| WaitForFirstVisuallyNonEmptyPaint(web_contents()); |
| ASSERT_TRUE(rfh_a->IsInBackForwardCache()); |
| EXPECT_EQ(web_contents()->GetThemeColor(), std::nullopt); |
| |
| ThemeColorObserver observer(web_contents()); |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| ASSERT_TRUE(observer.WaitUntilThemeColorChange()); |
| EXPECT_EQ(web_contents()->GetThemeColor(), 0xFFFF0000u); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| ContentsMimeTypeWhenRestoredFromCache) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // Navigate to A. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a); |
| EXPECT_EQ(web_contents()->GetContentsMimeType(), "text/html"); |
| |
| // Navigate to B. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| ASSERT_FALSE(delete_observer_rfh_a.deleted()); |
| EXPECT_TRUE(rfh_a->IsInBackForwardCache()); |
| |
| // Go back to A, which restores A from bfcache. ContentsMimeType should be |
| // restored as well. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| EXPECT_EQ(rfh_a, current_frame_host()); |
| ExpectRestored(FROM_HERE); |
| EXPECT_EQ(web_contents()->GetContentsMimeType(), "text/html"); |
| } |
| |
| // Check BackForwardCache is enabled and works for devices with very low memory. |
| // Navigate from A -> B and go back. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| BackForwardCacheEnabledOnLowMemoryDevices) { |
| // Set device physical memory to 10 MB. |
| blink::ApproximatedDeviceMemory::SetPhysicalMemoryMBForTesting(10); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // 1) Navigate to A. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameDeletedObserver delete_observer_rfh_a(current_frame_host()); |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| |
| // 2) Navigate to B. A should be in BackForwardCache. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| RenderFrameHostImpl* rfh_b = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_b(rfh_b); |
| EXPECT_FALSE(delete_observer_rfh_a.deleted()); |
| EXPECT_TRUE(rfh_a->IsInBackForwardCache()); |
| |
| // 3) Go back to A. B should be in BackForwardCache. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| EXPECT_FALSE(delete_observer_rfh_b.deleted()); |
| EXPECT_TRUE(rfh_b->IsInBackForwardCache()); |
| } |
| |
| // Test for functionality of memory controls in back-forward cache for low |
| // memory devices. |
| class BackForwardCacheBrowserTestForLowMemoryDevices |
| : public BackForwardCacheBrowserTest { |
| protected: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| BackForwardCacheBrowserTest::SetUpCommandLine(command_line); |
| |
| // Set the value of memory threshold more than the physical memory and check |
| // if back-forward cache is disabled or not. |
| std::string memory_threshold = |
| base::NumberToString(base::SysInfo::AmountOfPhysicalMemoryMB() + 1); |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| {{features::kBackForwardCacheMemoryControls, |
| {{"memory_threshold_for_back_forward_cache_in_mb", |
| memory_threshold}}}, |
| {blink::features::kLoadingTasksUnfreezable, {}}}, |
| {}); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Ensure that the BackForwardCache trial is not activated as expected on |
| // low-memory devices. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestForLowMemoryDevices, |
| DisableBFCacheForLowEndDevices) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // Ensure that the BackForwardCache trial starts inactive. |
| EXPECT_FALSE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial(features::kBackForwardCache) |
| ->trial_name())); |
| |
| EXPECT_FALSE(IsBackForwardCacheEnabled()); |
| |
| // Ensure that we do not activate the BackForwardCache trial when querying |
| // bfcache status. |
| EXPECT_FALSE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial(features::kBackForwardCache) |
| ->trial_name())); |
| |
| // 1) Navigate to A. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a); |
| |
| // 2) Navigate to B. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| |
| // 3) A shouldn't be stored in back-forward cache because the physical |
| // memory is less than the memory threshold. |
| delete_observer_rfh_a.WaitUntilDeleted(); |
| |
| // 4) Go back to check the |
| // NotRestoredReasons.kBackForwardCacheDisabledByLowMemory is recorded when |
| // the memory is less than the threshold value. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| |
| ExpectNotRestored( |
| { |
| BackForwardCacheMetrics::NotRestoredReason::kBackForwardCacheDisabled, |
| BackForwardCacheMetrics::NotRestoredReason:: |
| kBackForwardCacheDisabledByLowMemory, |
| }, |
| {}, {}, {}, {}, FROM_HERE); |
| |
| // Ensure that the BackForwardCache trial still hasn't been activated. |
| EXPECT_FALSE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial(features::kBackForwardCache) |
| ->trial_name())); |
| } |
| |
| // Trigger network reqeuests, then navigate from A to B, then go back. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestForLowMemoryDevices, |
| DisableBFCacheForLowEndDevices_NetworkRequests) { |
| net::test_server::ControllableHttpResponse image_response( |
| embedded_test_server(), "/image.png"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // Ensure that the trials starts inactive. |
| EXPECT_FALSE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial(features::kBackForwardCache) |
| ->trial_name())); |
| EXPECT_FALSE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial( |
| blink::features::kLoadingTasksUnfreezable) |
| ->trial_name())); |
| |
| EXPECT_FALSE(IsBackForwardCacheEnabled()); |
| |
| // Ensure that we do not activate the trials for kBackForwardCache and |
| // kLoadingTasksUnfreezable when querying bfcache or unfreezable loading tasks |
| // status. |
| EXPECT_FALSE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial(features::kBackForwardCache) |
| ->trial_name())); |
| EXPECT_FALSE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial( |
| blink::features::kLoadingTasksUnfreezable) |
| ->trial_name())); |
| |
| // 1) Navigate to A. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a); |
| |
| // Request for an image and send a response to trigger loading code. This is |
| // to ensure kLoadingTasksUnfreezable won't trigger bfcache activation. |
| EXPECT_TRUE(ExecJs(rfh_a, R"( |
| var image = document.createElement("img"); |
| image.src = "image.png"; |
| document.body.appendChild(image); |
| )")); |
| image_response.WaitForRequest(); |
| image_response.Send(net::HTTP_OK, "image/png"); |
| image_response.Send("image_body"); |
| image_response.Done(); |
| |
| // 2) Navigate to B. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| |
| // 3) A shouldn't be stored in back-forward cache because the physical |
| // memory is less than the memory threshold. |
| delete_observer_rfh_a.WaitUntilDeleted(); |
| |
| // Nothing is recorded when the memory is less than the threshold value. |
| ExpectOutcomeDidNotChange(FROM_HERE); |
| ExpectNotRestoredDidNotChange(FROM_HERE); |
| |
| // Ensure that the trials still haven't been activated. |
| EXPECT_FALSE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial(features::kBackForwardCache) |
| ->trial_name())); |
| EXPECT_FALSE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial( |
| blink::features::kLoadingTasksUnfreezable) |
| ->trial_name())); |
| } |
| |
| // Test for functionality of memory controls in back-forward cache for high |
| // memory devices. |
| class BackForwardCacheBrowserTestForHighMemoryDevices |
| : public BackForwardCacheBrowserTest { |
| protected: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| BackForwardCacheBrowserTest::SetUpCommandLine(command_line); |
| |
| // Set the value of memory threshold less than the physical memory and check |
| // if back-forward cache is enabled or not. |
| std::string memory_threshold = |
| base::NumberToString(base::SysInfo::AmountOfPhysicalMemoryMB() - 1); |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| {{features::kBackForwardCacheMemoryControls, |
| {{"memory_threshold_for_back_forward_cache_in_mb", |
| memory_threshold}}}, |
| {blink::features::kLoadingTasksUnfreezable, {}}}, |
| {}); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Ensure that the BackForwardCache trial got activated as expected on |
| // high-memory devices when the BackForwardCache feature is enabled. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestForHighMemoryDevices, |
| EnableBFCacheForHighMemoryDevices) { |
| // Ensure that the BackForwardCache trial starts active on high-memory devices |
| // when the BackForwardCache feature is enabled, because |
| // IsBackForwardCacheEnabled() got queried already before the test starts. |
| EXPECT_TRUE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial(features::kBackForwardCache) |
| ->trial_name())); |
| |
| EXPECT_TRUE(IsBackForwardCacheEnabled()); |
| |
| // Ensure that the BackForwardCache trial stays active after querying |
| // IsBackForwardCacheEnabled(). |
| EXPECT_TRUE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial(features::kBackForwardCache) |
| ->trial_name())); |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // 1) Navigate to A. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| |
| // 2) Navigate to B. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| |
| // 3) A should be stored in back-forward cache because the physical memory is |
| // greater than the memory threshold. |
| EXPECT_TRUE(rfh_a->IsInBackForwardCache()); |
| |
| // Ensure that the BackForwardCache trial stays active. |
| EXPECT_TRUE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial(features::kBackForwardCache) |
| ->trial_name())); |
| } |
| |
| // Trigger network reqeuests, then navigate from A to B, then go back. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestForHighMemoryDevices, |
| EnableBFCacheForHighMemoryDevices_NetworkRequests) { |
| net::test_server::ControllableHttpResponse image_response( |
| embedded_test_server(), "/image.png"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // Ensure that back-forward cache flag is enabled and the trial is active. |
| EXPECT_TRUE(IsBackForwardCacheEnabled()); |
| EXPECT_TRUE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial(features::kBackForwardCache) |
| ->trial_name())); |
| |
| // Ensure that the LoadingTasksUnfreezable trials starts as inactive. |
| EXPECT_FALSE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial( |
| blink::features::kLoadingTasksUnfreezable) |
| ->trial_name())); |
| |
| // 1) Navigate to A. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a); |
| |
| // Request for an image and send a response to trigger loading code. |
| EXPECT_TRUE(ExecJs(rfh_a, R"( |
| var image = document.createElement("img"); |
| image.src = "image.png"; |
| document.body.appendChild(image); |
| )")); |
| image_response.WaitForRequest(); |
| image_response.Send(net::HTTP_OK, "image/png"); |
| image_response.Send("image_body"); |
| image_response.Done(); |
| |
| // The loading code activates the LoadingTasksUnfreezable trial. |
| EXPECT_TRUE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial( |
| blink::features::kLoadingTasksUnfreezable) |
| ->trial_name())); |
| |
| // 2) Navigate to B. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| |
| // 3) A should be stored in back-forward cache because the physical memory is |
| // greater than the memory threshold. |
| EXPECT_TRUE(rfh_a->IsInBackForwardCache()); |
| |
| // Ensure that the trials stay activated. |
| EXPECT_TRUE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial(features::kBackForwardCache) |
| ->trial_name())); |
| EXPECT_TRUE(base::FieldTrialList::IsTrialActive( |
| base::FeatureList::GetFieldTrial( |
| blink::features::kLoadingTasksUnfreezable) |
| ->trial_name())); |
| } |
| |
| // Tests for high memory devices that have the BackForwardCache feature flag |
| // disabled. |
| class BackForwardCacheBrowserTestForHighMemoryDevicesWithBFCacheDisabled |
| : public BackForwardCacheBrowserTest { |
| protected: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| BackForwardCacheBrowserTest::SetUpCommandLine(command_line); |
| |
| // Set the value of memory threshold less than the physical memory and check |
| // if back-forward cache is enabled or not. |
| std::string memory_threshold = |
| base::NumberToString(base::SysInfo::AmountOfPhysicalMemoryMB() - 1); |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| /*enabled_features=*/ |
| {{features::kBackForwardCacheMemoryControls, |
| {{"memory_threshold_for_back_forward_cache_in_mb", |
| memory_threshold}}}, |
| {blink::features::kLoadingTasksUnfreezable, {}}}, |
| /*disabled_features=*/ |
| {features::kBackForwardCache}); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F( |
| BackForwardCacheBrowserTestForHighMemoryDevicesWithBFCacheDisabled, |
| HighMemoryDevicesWithBFacheDisabled) { |
| // Ensure that IsBackForwardCacheEnabled() returns false, because the |
| // BackForwardCache feature is disabled. |
| EXPECT_FALSE(IsBackForwardCacheEnabled()); |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // 1) Navigate to A. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a); |
| |
| // 2) Navigate to B. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| |
| // 3) A shouldn't be stored in back-forward cache because the BackForwardCache |
| // feature is disabled. |
| delete_observer_rfh_a.WaitUntilDeleted(); |
| |
| // 4) Go back to check that only kBackForwardCacheDisabled is recorded. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| |
| ExpectNotRestored( |
| { |
| BackForwardCacheMetrics::NotRestoredReason::kBackForwardCacheDisabled, |
| }, |
| {}, {}, {}, {}, FROM_HERE); |
| } |
| |
| // Start an inifite dialogs in JS, yielding after each. The first dialog should |
| // be dismissed by navigation. The later dialogs should be handled gracefully |
| // and not appear while in BFCache. Finally, when the page comes out of BFCache, |
| // dialogs should appear again. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| CanUseCacheWhenPageAlertsInTimeoutLoop) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // Navigate to A. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a); |
| |
| AppModalDialogWaiter dialog_waiter(shell()); |
| |
| EXPECT_TRUE(ExecJs(rfh_a, R"( |
| function alertLoop() { |
| setTimeout(alertLoop, 0); |
| window.alert("alert"); |
| } |
| // Don't block this script. |
| setTimeout(alertLoop, 0); |
| )")); |
| |
| dialog_waiter.Wait(); |
| |
| // Navigate to B. |
| ASSERT_TRUE(NavigateToURL(shell(), url_b)); |
| RenderFrameHostImpl* rfh_b = current_frame_host(); |
| |
| ASSERT_FALSE(delete_observer_rfh_a.deleted()); |
| ASSERT_THAT(rfh_a, InBackForwardCache()); |
| ASSERT_NE(rfh_a, rfh_b); |
| |
| dialog_waiter.Restart(); |
| |
| // Go back. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| EXPECT_EQ(rfh_a, current_frame_host()); |
| EXPECT_FALSE(rfh_a->IsInBackForwardCache()); |
| |
| // The page should still be requesting dialogs in a loop. Wait for one to be |
| // requested. |
| dialog_waiter.Wait(); |
| } |
| |
| // UnloadOldFrame will clear all dialogs. We test that further requests for |
| // dialogs coming from JS do not result in the creation of a dialog. This test |
| // posts some dialog creation JS to the render from inside the |
| // CommitNavigationCallback task. This JS is then able to post a task back to |
| // the renders to show a dialog. By the time this task runs, we the |
| // RenderFrameHostImpl's is_active() should be false. |
| // |
| // This test is not perfect, it can pass simply because the renderer thread does |
| // not run the JS in time. Ideally it would block until the renderer posts the |
| // request for a dialog but it's possible to do that without creating a nested |
| // message loop and if we do that, we risk processing the dialog request. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| DialogsCancelledAndSuppressedWhenCached) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // Navigate to A. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a); |
| |
| // Let's us know whether the following callback ran. Not strictly necessary |
| // since it really should run. |
| bool posted_dialog_js = false; |
| // Create a callback that will be called during the DidCommitNavigation task. |
| WillEnterBackForwardCacheCallbackForTesting |
| will_enter_back_forward_cache_callback = |
| base::BindLambdaForTesting([&]() { |
| // Post a dialog, it should not result in a dialog being created. |
| ExecuteScriptAsync(rfh_a, R"(window.alert("alert");)"); |
| posted_dialog_js = true; |
| }); |
| rfh_a->render_view_host()->SetWillEnterBackForwardCacheCallbackForTesting( |
| will_enter_back_forward_cache_callback); |
| |
| AppModalDialogWaiter dialog_waiter(shell()); |
| |
| // Try show another dialog. It should work. |
| ExecuteScriptAsync(rfh_a, R"(window.alert("alert");)"); |
| dialog_waiter.Wait(); |
| |
| dialog_waiter.Restart(); |
| |
| // Navigate to B. |
| ASSERT_TRUE(NavigateToURL(shell(), url_b)); |
| RenderFrameHostImpl* rfh_b = current_frame_host(); |
| |
| ASSERT_FALSE(delete_observer_rfh_a.deleted()); |
| ASSERT_THAT(rfh_a, InBackForwardCache()); |
| ASSERT_NE(rfh_a, rfh_b); |
| // Test that the JS was run and that it didn't result in a dialog. |
| ASSERT_TRUE(posted_dialog_js); |
| ASSERT_FALSE(dialog_waiter.WasDialogRequestedCallbackCalled()); |
| |
| // Go back. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| |
| EXPECT_EQ(rfh_a, current_frame_host()); |
| EXPECT_FALSE(rfh_a->IsInBackForwardCache()); |
| |
| // Try show another dialog. It should work. |
| ExecuteScriptAsync(rfh_a, R"(window.alert("alert");)"); |
| dialog_waiter.Wait(); |
| } |
| |
| // Tests that pagehide handlers of the old RFH are run for bfcached pages even |
| // if the page is already hidden (and visibilitychange won't run). |
| // Disabled on Linux and Win because of flakiness, see crbug.com/40165901. |
| #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN) |
| #define MAYBE_PagehideRunsWhenPageIsHidden DISABLED_PagehideRunsWhenPageIsHidden |
| #else |
| #define MAYBE_PagehideRunsWhenPageIsHidden PagehideRunsWhenPageIsHidden |
| #endif |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| MAYBE_PagehideRunsWhenPageIsHidden) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_1(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| GURL url_3(embedded_test_server()->GetURL("a.com", "/title2.html")); |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| // 1) Navigate to |url_1| and hide the tab. |
| EXPECT_TRUE(NavigateToURL(shell(), url_1)); |
| RenderFrameHostImplWrapper main_frame_1(web_contents->GetPrimaryMainFrame()); |
| // We need to set it to Visibility::VISIBLE first in case this is the first |
| // time the visibility is updated. |
| web_contents->UpdateWebContentsVisibility(Visibility::VISIBLE); |
| web_contents->UpdateWebContentsVisibility(Visibility::HIDDEN); |
| EXPECT_EQ(Visibility::HIDDEN, web_contents->GetVisibility()); |
| |
| // Create a pagehide handler that sets item "pagehide_storage" and a |
| // visibilitychange handler that sets item "visibilitychange_storage" in |
| // localStorage. |
| EXPECT_TRUE(ExecJs(main_frame_1.get(), |
| R"( |
| localStorage.setItem('pagehide_storage', 'not_dispatched'); |
| var dispatched_pagehide = false; |
| window.onpagehide = function(e) { |
| if (dispatched_pagehide) { |
| // We shouldn't dispatch pagehide more than once. |
| localStorage.setItem('pagehide_storage', 'dispatched_more_than_once'); |
| } else if (!e.persisted) { |
| localStorage.setItem('pagehide_storage', 'wrong_persisted'); |
| } else { |
| localStorage.setItem('pagehide_storage', 'dispatched_once'); |
| } |
| dispatched_pagehide = true; |
| } |
| localStorage.setItem('visibilitychange_storage', 'not_dispatched'); |
| document.onvisibilitychange = function(e) { |
| localStorage.setItem('visibilitychange_storage', |
| 'should_not_be_dispatched'); |
| } |
| )")); |
| // |visibilitychange_storage| should be set to its initial correct value. |
| EXPECT_EQ("not_dispatched", |
| GetLocalStorage(main_frame_1.get(), "visibilitychange_storage")); |
| |
| // 2) Navigate cross-site to |url_2|. We need to navigate cross-site to make |
| // sure we won't run pagehide and visibilitychange during new page's commit, |
| // which is tested in ProactivelySwapBrowsingInstancesSameSiteTest. |
| EXPECT_TRUE(NavigateToURL(shell(), url_2)); |
| |
| // |main_frame_1| should be in the back-forward cache. |
| EXPECT_TRUE(main_frame_1->IsInBackForwardCache()); |
| |
| // 3) Navigate to |url_3| which is same-origin with |url_1|, so we can check |
| // the localStorage values. |
| EXPECT_TRUE(NavigateToURL(shell(), url_3)); |
| RenderFrameHostImpl* main_frame_3 = web_contents->GetPrimaryMainFrame(); |
| |
| // Check that the value for 'pagehide_storage' and 'visibilitychange_storage' |
| // are set correctly. |
| EXPECT_TRUE( |
| WaitForLocalStorage(main_frame_3, "pagehide_storage", "dispatched_once")); |
| EXPECT_TRUE(WaitForLocalStorage(main_frame_3, "visibilitychange_storage", |
| "not_dispatched")); |
| } |
| |
| // Tests that we're getting the correct TextInputState and focus updates when a |
| // page enters the back-forward cache and when it gets restored. |
| // TODO(b/324570785): Re-enable the test for Android. |
| #if BUILDFLAG(IS_ANDROID) |
| #define MAYBE_TextInputStateUpdated DISABLED_TextInputStateUpdated |
| #else |
| #define MAYBE_TextInputStateUpdated TextInputStateUpdated |
| #endif |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| MAYBE_TextInputStateUpdated) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_1(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| |
| // 1) Navigate to |url_1| and add a text input with "foo" as the value. |
| EXPECT_TRUE(NavigateToURL(shell(), url_1)); |
| RenderFrameHostImpl* rfh_1 = current_frame_host(); |
| EXPECT_TRUE(ExecJs(rfh_1, |
| "document.title='bfcached';" |
| "var input = document.createElement('input');" |
| "input.setAttribute('type', 'text');" |
| "input.setAttribute('value', 'foo');" |
| "document.body.appendChild(input);" |
| "var focusCount = 0;" |
| "var blurCount = 0;" |
| "input.onfocus = () => { focusCount++;};" |
| "input.onblur = () => { blurCount++; };")); |
| |
| { |
| TextInputManagerTypeObserver type_observer(web_contents(), |
| ui::TEXT_INPUT_TYPE_TEXT); |
| TextInputManagerValueObserver value_observer(web_contents(), "foo"); |
| // 2) Press tab key to focus the <input>, and verify the type & value. |
| SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB, |
| ui::VKEY_TAB, false, false, false, false); |
| type_observer.Wait(); |
| value_observer.Wait(); |
| |
| EXPECT_EQ(rfh_1, web_contents()->GetFocusedFrame()); |
| EXPECT_EQ(EvalJs(rfh_1, "focusCount").ExtractInt(), 1); |
| EXPECT_EQ(EvalJs(rfh_1, "blurCount").ExtractInt(), 0); |
| } |
| |
| { |
| TextInputManagerTester tester(web_contents()); |
| TextInputManagerValueObserver value_observer(web_contents(), "A"); |
| // 3) Press the "A" key to change the text input value. This should notify |
| // the browser that the text input value has changed. |
| SimulateKeyPress(web_contents(), ui::DomKey::FromCharacter('A'), |
| ui::DomCode::US_A, ui::VKEY_A, false, false, false, false); |
| value_observer.Wait(); |
| |
| EXPECT_EQ(rfh_1, web_contents()->GetFocusedFrame()); |
| EXPECT_EQ(EvalJs(rfh_1, "focusCount").ExtractInt(), 1); |
| EXPECT_EQ(EvalJs(rfh_1, "blurCount").ExtractInt(), 0); |
| } |
| |
| { |
| TextInputManagerTypeObserver type_observer(web_contents(), |
| ui::TEXT_INPUT_TYPE_NONE); |
| // 4) Navigating to |url_2| should reset type to TEXT_INPUT_TYPE_NONE. |
| EXPECT_TRUE(NavigateToURL(shell(), url_2)); |
| type_observer.Wait(); |
| // |rfh_1| should get into the back-forward cache. |
| EXPECT_TRUE(rfh_1->IsInBackForwardCache()); |
| EXPECT_EQ(current_frame_host(), web_contents()->GetFocusedFrame()); |
| EXPECT_NE(rfh_1, web_contents()->GetFocusedFrame()); |
| } |
| |
| { |
| // 5) Navigating back to |url_1|, we shouldn't restore the focus to the |
| // text input, but |rfh_1| will be focused again as we will restore focus |
| // to main frame after navigation. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| |
| EXPECT_EQ(rfh_1, web_contents()->GetFocusedFrame()); |
| EXPECT_EQ(EvalJs(rfh_1, "focusCount").ExtractInt(), 1); |
| EXPECT_EQ(EvalJs(rfh_1, "blurCount").ExtractInt(), 1); |
| } |
| |
| { |
| TextInputManagerTypeObserver type_observer(web_contents(), |
| ui::TEXT_INPUT_TYPE_TEXT); |
| TextInputManagerValueObserver value_observer(web_contents(), "A"); |
| // 6) Press tab key to focus the <input> again. Note that we need to press |
| // the tab key twice here, because the last "tab focus" point was the |
| // <input> element. The first tab key press would focus on the UI/url bar, |
| // then the second tab key would go back to the <input>. |
| SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB, |
| ui::VKEY_TAB, false, false, false, false); |
| SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB, |
| ui::VKEY_TAB, false, false, false, false); |
| type_observer.Wait(); |
| value_observer.Wait(); |
| |
| EXPECT_EQ(rfh_1, web_contents()->GetFocusedFrame()); |
| EXPECT_EQ(EvalJs(rfh_1, "focusCount").ExtractInt(), 2); |
| EXPECT_EQ(EvalJs(rfh_1, "blurCount").ExtractInt(), 1); |
| } |
| } |
| |
| #if (BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID)) |
| #define MAYBE_SubframeTextInputStateUpdated DISABLED_SubframeTextInputStateUpdated |
| #else |
| #define MAYBE_SubframeTextInputStateUpdated SubframeTextInputStateUpdated |
| #endif |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| MAYBE_SubframeTextInputStateUpdated) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_1(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b(a))")); |
| GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| |
| // 1) Navigate to |url_1| and add a text input with "foo" as the value in the |
| // a.com subframe. |
| EXPECT_TRUE(NavigateToURL(shell(), url_1)); |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| RenderFrameHostImpl* rfh_b = rfh_a->child_at(0)->current_frame_host(); |
| RenderFrameHostImpl* rfh_subframe_a = |
| rfh_b->child_at(0)->current_frame_host(); |
| EXPECT_TRUE(ExecJs(rfh_subframe_a, |
| "var input = document.createElement('input');" |
| "input.setAttribute('type', 'text');" |
| "input.setAttribute('value', 'foo');" |
| "document.body.appendChild(input);" |
| "var focusCount = 0;" |
| "var blurCount = 0;" |
| "input.onfocus = () => { focusCount++;};" |
| "input.onblur = () => { blurCount++; };")); |
| |
| { |
| TextInputManagerTypeObserver type_observer(web_contents(), |
| ui::TEXT_INPUT_TYPE_TEXT); |
| TextInputManagerValueObserver value_observer(web_contents(), "foo"); |
| // 2) Press tab key to focus the <input>, and verify the type & value. |
| SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB, |
| ui::VKEY_TAB, false, false, false, false); |
| type_observer.Wait(); |
| value_observer.Wait(); |
| |
| EXPECT_EQ(rfh_subframe_a, web_contents()->GetFocusedFrame()); |
| EXPECT_EQ(EvalJs(rfh_subframe_a, "focusCount").ExtractInt(), 1); |
| EXPECT_EQ(EvalJs(rfh_subframe_a, "blurCount").ExtractInt(), 0); |
| } |
| |
| { |
| TextInputManagerTester tester(web_contents()); |
| TextInputManagerValueObserver value_observer(web_contents(), "A"); |
| // 3) Press the "A" key to change the text input value. This should notify |
| // the browser that the text input value has changed. |
| SimulateKeyPress(web_contents(), ui::DomKey::FromCharacter('A'), |
| ui::DomCode::US_A, ui::VKEY_A, false, false, false, false); |
| value_observer.Wait(); |
| |
| EXPECT_EQ(rfh_subframe_a, web_contents()->GetFocusedFrame()); |
| EXPECT_EQ(EvalJs(rfh_subframe_a, "focusCount").ExtractInt(), 1); |
| EXPECT_EQ(EvalJs(rfh_subframe_a, "blurCount").ExtractInt(), 0); |
| } |
| |
| { |
| TextInputManagerTypeObserver type_observer(web_contents(), |
| ui::TEXT_INPUT_TYPE_NONE); |
| // 4) Navigating to |url_2| should reset type to TEXT_INPUT_TYPE_NONE and |
| // changed focus to the new page's main frame. |
| EXPECT_TRUE(NavigateToURL(shell(), url_2)); |
| type_observer.Wait(); |
| |
| // |rfh_a| and its subframes should get into the back-forward cache. |
| EXPECT_TRUE(rfh_a->IsInBackForwardCache()); |
| EXPECT_TRUE(rfh_b->IsInBackForwardCache()); |
| EXPECT_TRUE(rfh_subframe_a->IsInBackForwardCache()); |
| EXPECT_NE(rfh_subframe_a, web_contents()->GetFocusedFrame()); |
| } |
| |
| { |
| // 5) Navigating back to |url_1|, we shouldn't restore the focus to the |
| // text input in the subframe (we will focus on the main frame |rfh_a| |
| // instead). |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| |
| EXPECT_EQ(rfh_a, web_contents()->GetFocusedFrame()); |
| EXPECT_EQ(EvalJs(rfh_subframe_a, "focusCount").ExtractInt(), 1); |
| EXPECT_EQ(EvalJs(rfh_subframe_a, "blurCount").ExtractInt(), 1); |
| } |
| |
| { |
| TextInputManagerTypeObserver type_observer(web_contents(), |
| ui::TEXT_INPUT_TYPE_TEXT); |
| TextInputManagerValueObserver value_observer(web_contents(), "A"); |
| // 6) Press tab key to focus the <input> again. |
| SimulateKeyPress(web_contents(), ui::DomKey::TAB, ui::DomCode::TAB, |
| ui::VKEY_TAB, false, false, false, false); |
| type_observer.Wait(); |
| value_observer.Wait(); |
| |
| EXPECT_EQ(rfh_subframe_a, web_contents()->GetFocusedFrame()); |
| EXPECT_EQ(EvalJs(rfh_subframe_a, "focusCount").ExtractInt(), 2); |
| EXPECT_EQ(EvalJs(rfh_subframe_a, "blurCount").ExtractInt(), 1); |
| } |
| } |
| |
| // Tests that trying to focus on a BFCached cross-site iframe won't crash. |
| // See https://crbug.com/1250218. |
| // TODO(crbug.com/40856039): Flaky on linux tsan |
| #if BUILDFLAG(IS_LINUX) && defined(THREAD_SANITIZER) |
| #define MAYBE_FocusSameSiteSubframeOnPagehide \ |
| DISABLED_FocusSameSiteSubframeOnPagehide |
| #else |
| #define MAYBE_FocusSameSiteSubframeOnPagehide FocusSameSiteSubframeOnPagehide |
| #endif |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| MAYBE_FocusSameSiteSubframeOnPagehide) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL main_url( |
| embedded_test_server()->GetURL("a.com", "/page_with_iframe.html")); |
| GURL main_url_2(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| |
| // 1) Navigate to a page with a same-site iframe. |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImplWrapper rfh_1(current_frame_host()); |
| EXPECT_EQ(rfh_1.get(), web_contents()->GetFocusedFrame()); |
| |
| // 2) Navigate away from the page while trying to focus the subframe on |
| // pagehide. The DidFocusFrame IPC should arrive after the page gets into |
| // BFCache and should be ignored by the browser. The focus after navigation |
| // should go to the new main frame. |
| EXPECT_TRUE(ExecJs(rfh_1.get(), R"( |
| window.onpagehide = function(e) { |
| document.getElementById("test_iframe").focus(); |
| })")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url_2)); |
| EXPECT_TRUE(rfh_1->IsInBackForwardCache()); |
| EXPECT_NE(rfh_1.get(), web_contents()->GetFocusedFrame()); |
| EXPECT_EQ(current_frame_host(), web_contents()->GetFocusedFrame()); |
| |
| // 3) Navigate back to the page. The focus should be on the main frame. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| EXPECT_EQ(rfh_1.get(), web_contents()->GetFocusedFrame()); |
| ExpectRestored(FROM_HERE); |
| } |
| |
| // Tests that trying to focus on a BFCached cross-site iframe won't crash. |
| // See https://crbug.com/1250218. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| FocusCrossSiteSubframeOnPagehide) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| GURL main_url_2(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| |
| // 1) Navigate to a page with a cross-site iframe. |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImplWrapper rfh_1(current_frame_host()); |
| EXPECT_EQ(rfh_1.get(), web_contents()->GetFocusedFrame()); |
| |
| // 2) Navigate away from the page while trying to focus the subframe on |
| // pagehide. The DidFocusFrame IPC should arrive after the page gets into |
| // BFCache and should be ignored by the browser. The focus after navigation |
| // should go to the new main frame. |
| EXPECT_TRUE(ExecJs(rfh_1.get(), R"( |
| window.onpagehide = function(e) { |
| document.getElementById("child-0").focus(); |
| })")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url_2)); |
| EXPECT_TRUE(rfh_1->IsInBackForwardCache()); |
| EXPECT_NE(rfh_1.get(), web_contents()->GetFocusedFrame()); |
| |
| // 3) Navigate back to the page. The focus should be on the original page's |
| // main frame. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| EXPECT_EQ(rfh_1.get(), current_frame_host()); |
| ExpectRestored(FROM_HERE); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| MainDocumentCSPHeadersAreRestored) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url_a(embedded_test_server()->GetURL( |
| "a.com", |
| "/set-header?" |
| "Content-Security-Policy: frame-src 'none'")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // 1) Navigate to A, which should set CSP. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| |
| // Check that CSP was set. |
| { |
| const std::vector<network::mojom::ContentSecurityPolicyPtr>& root_csp = |
| current_frame_host() |
| ->policy_container_host() |
| ->policies() |
| .content_security_policies; |
| EXPECT_EQ(1u, root_csp.size()); |
| EXPECT_EQ("frame-src 'none'", root_csp[0]->header->header_value); |
| } |
| |
| // 2) Navigate to B. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| |
| // 3) Navigate back and expect that the CSP headers are present on the main |
| // frame. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| EXPECT_EQ(rfh_a, current_frame_host()); |
| ExpectRestored(FROM_HERE); |
| |
| // Check that CSP was restored. |
| { |
| const std::vector<network::mojom::ContentSecurityPolicyPtr>& root_csp = |
| current_frame_host() |
| ->policy_container_host() |
| ->policies() |
| .content_security_policies; |
| EXPECT_EQ(1u, root_csp.size()); |
| EXPECT_EQ("frame-src 'none'", root_csp[0]->header->header_value); |
| } |
| } |
| |
| // Check that sandboxed documents are cached and won't lose their sandbox flags |
| // after restoration. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, CspSandbox) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url_a( |
| embedded_test_server()->GetURL("a.com", |
| "/set-header?" |
| "Content-Security-Policy: sandbox")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // 1) Navigate to A, which should set CSP. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| RenderFrameDeletedObserver delete_observer_rfh_a(rfh_a); |
| { |
| const std::vector<network::mojom::ContentSecurityPolicyPtr>& root_csp = |
| current_frame_host() |
| ->policy_container_host() |
| ->policies() |
| .content_security_policies; |
| ASSERT_EQ(1u, root_csp.size()); |
| ASSERT_EQ("sandbox", root_csp[0]->header->header_value); |
| ASSERT_EQ(network::mojom::WebSandboxFlags::kAll, |
| current_frame_host()->active_sandbox_flags()); |
| } |
| |
| // 2) Navigate to B. Expect the previous RenderFrameHost to enter the bfcache. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| EXPECT_FALSE(delete_observer_rfh_a.deleted()); |
| EXPECT_TRUE(rfh_a->IsInBackForwardCache()); |
| { |
| const std::vector<network::mojom::ContentSecurityPolicyPtr>& root_csp = |
| current_frame_host() |
| ->policy_container_host() |
| ->policies() |
| .content_security_policies; |
| ASSERT_EQ(0u, root_csp.size()); |
| ASSERT_EQ(network::mojom::WebSandboxFlags::kNone, |
| current_frame_host()->active_sandbox_flags()); |
| } |
| |
| // 3) Navigate back and expect the page to be restored, with the correct |
| // CSP and sandbox flags. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| EXPECT_FALSE(delete_observer_rfh_a.deleted()); |
| EXPECT_EQ(current_frame_host(), rfh_a); |
| { |
| const std::vector<network::mojom::ContentSecurityPolicyPtr>& root_csp = |
| current_frame_host() |
| ->policy_container_host() |
| ->policies() |
| .content_security_policies; |
| ASSERT_EQ(1u, root_csp.size()); |
| ASSERT_EQ("sandbox", root_csp[0]->header->header_value); |
| ASSERT_EQ(network::mojom::WebSandboxFlags::kAll, |
| current_frame_host()->active_sandbox_flags()); |
| } |
| } |
| |
| // Check that about:blank is not cached. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, AboutBlankWillNotBeCached) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // 1) Navigate to about:blank. |
| GURL blank_url(url::kAboutBlankURL); |
| EXPECT_TRUE(NavigateToURL(shell(), blank_url)); |
| RenderFrameHostImplWrapper rfh_blank(current_frame_host()); |
| |
| // 2) Navigate to a.com. |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/empty.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| |
| // 3) Navigate back to about:blank. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| |
| // This about:blank document does not have a SiteInstance and then loading a |
| // page on it doesn't swap the browsing instance. |
| |
| if (ShouldCreateNewHostForAllFrames()) { |
| EXPECT_TRUE(rfh_blank.WaitUntilRenderFrameDeleted()); |
| ExpectNotRestored( |
| { |
| BackForwardCacheMetrics::NotRestoredReason::kHTTPStatusNotOK, |
| BackForwardCacheMetrics::NotRestoredReason::kSchemeNotHTTPOrHTTPS, |
| BackForwardCacheMetrics::NotRestoredReason:: |
| kBrowsingInstanceNotSwapped, |
| }, |
| {}, {ShouldSwapBrowsingInstance::kNo_DoesNotHaveSite}, {}, {}, |
| FROM_HERE); |
| |
| } else { |
| EXPECT_FALSE(rfh_blank->IsInBackForwardCache()); |
| ExpectNotRestored( |
| { |
| BackForwardCacheMetrics::NotRestoredReason:: |
| kBrowsingInstanceNotSwapped, |
| }, |
| {}, {ShouldSwapBrowsingInstance::kNo_DoesNotHaveSite}, {}, {}, |
| FROM_HERE); |
| } |
| } |
| |
| // Check that browsing instances are not swapped when a navigation redirects |
| // toward the last committed URL and the reasons are recorded correctly. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, RedirectToSelf) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| NavigationControllerImpl& controller = web_contents()->GetController(); |
| |
| // 1) Navigate to a.com/empty.html. |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/empty.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImplWrapper rfh_a(current_frame_host()); |
| EXPECT_EQ(1, controller.GetEntryCount()); |
| EXPECT_EQ(url_a, controller.GetLastCommittedEntry()->GetURL()); |
| |
| // 2) Navigate to the same page by redirection. |
| GURL url_a2(embedded_test_server()->GetURL( |
| "a.com", "/server-redirect-301?" + url_a.spec())); |
| EXPECT_TRUE(NavigateToURL(shell(), url_a2, url_a)); |
| RenderFrameHostImplWrapper rfh_b(current_frame_host()); |
| EXPECT_EQ(2, controller.GetEntryCount()); |
| |
| if (ShouldCreateNewHostForAllFrames()) { |
| EXPECT_TRUE(rfh_a.WaitUntilRenderFrameDeleted()); |
| } else { |
| EXPECT_FALSE(rfh_a->IsInBackForwardCache()); |
| EXPECT_TRUE(rfh_a->GetSiteInstance()->IsRelatedSiteInstance( |
| rfh_b->GetSiteInstance())); |
| } |
| |
| EXPECT_EQ(url_a, controller.GetLastCommittedEntry()->GetURL()); |
| |
| // 3) Navigate back to the previous page. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| EXPECT_EQ(2, controller.GetEntryCount()); |
| EXPECT_EQ(url_a, controller.GetLastCommittedEntry()->GetURL()); |
| |
| // TODO(crbug.com/40760515): Investigate whether these navigation results are |
| // expected. |
| ExpectNotRestored( |
| { |
| BackForwardCacheMetrics::NotRestoredReason:: |
| kBrowsingInstanceNotSwapped, |
| }, |
| {}, {ShouldSwapBrowsingInstance::kNo_SameUrlNavigation}, {}, {}, |
| FROM_HERE); |
| } |
| |
| // Check that reloading doesn't affect the back-forward cache usage. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, ReloadDoesntAffectCache) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| NavigationControllerImpl& controller = web_contents()->GetController(); |
| |
| // 1) Navigate to a.com. |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/empty.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| EXPECT_EQ(1, controller.GetEntryCount()); |
| EXPECT_EQ(url_a, controller.GetLastCommittedEntry()->GetURL()); |
| |
| // 2) Navigate to b.com. |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/empty.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| EXPECT_EQ(2, controller.GetEntryCount()); |
| EXPECT_EQ(url_b, controller.GetLastCommittedEntry()->GetURL()); |
| |
| // 3) Go back to a.com and reload. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| EXPECT_EQ(2, controller.GetEntryCount()); |
| EXPECT_EQ(url_a, controller.GetLastCommittedEntry()->GetURL()); |
| |
| ExpectRestored(FROM_HERE); |
| |
| // 4) Reload the tab. |
| web_contents()->GetController().Reload(content::ReloadType::NORMAL, false); |
| EXPECT_TRUE(WaitForLoadStop(web_contents())); |
| EXPECT_EQ(2, controller.GetEntryCount()); |
| EXPECT_EQ(url_a, controller.GetLastCommittedEntry()->GetURL()); |
| |
| // By reloading the tab, ShouldSwapBrowsingInstance:: |
| // kNo_AlreadyHasMatchingBrowsingInstance is set once. This should be reset |
| // when the navigation 4)'s commit finishes and should not prevent putting the |
| // page into the back-forward cache. |
| // |
| // Note that SetBrowsingInstanceSwapResult might not be called for every |
| // navigation because we might not get to this point for some navigations, |
| // e.g. if the navigation uses a pre-existing RenderFrameHost and SiteInstance |
| // for navigation. |
| // |
| // TODO(crbug.com/40747698): Tie BrowsingInstanceSwapResult to |
| // NavigationRequest instead and move the SetBrowsingInstanceSwapResult call |
| // for navigations to happen at commit time instead. |
| |
| // 5) Go forward to b.com and reload. |
| ASSERT_TRUE(HistoryGoForward(web_contents())); |
| EXPECT_EQ(2, controller.GetEntryCount()); |
| EXPECT_EQ(url_b, controller.GetLastCommittedEntry()->GetURL()); |
| |
| // The page loaded at B) is correctly cached and restored. Reloading doesn't |
| // affect the cache usage. |
| ExpectRestored(FROM_HERE); |
| |
| // 6) Go back to a.com. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| EXPECT_EQ(2, controller.GetEntryCount()); |
| EXPECT_EQ(url_a, controller.GetLastCommittedEntry()->GetURL()); |
| |
| // The page loaded at 3) is correctly cached and restored. Reloading doesn't |
| // affect the cache usage. |
| ExpectRestored(FROM_HERE); |
| } |
| |
| // Regression test for crbug.com/1183313. Checks that CommitNavigationParam's |
| // |has_user_gesture| value reflects the gesture from the latest navigation |
| // after the commit finished. |
| IN_PROC_BROWSER_TEST_F( |
| BackForwardCacheBrowserTest, |
| SameDocumentNavAfterRestoringDocumentLoadedWithUserGesture) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL start_url(embedded_test_server()->GetURL("/title1.html")); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_a_foo(embedded_test_server()->GetURL("a.com", "/title1.html#foo")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| NavigationControllerImpl& controller = web_contents()->GetController(); |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| |
| // Initial navigation (so that we can initiate a navigation from renderer). |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| // 1) Navigate to A with user gesture. |
| { |
| FrameNavigateParamsCapturer params_capturer(root); |
| EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url_a)); |
| params_capturer.Wait(); |
| EXPECT_TRUE(params_capturer.has_user_gesture()); |
| EXPECT_TRUE(root->current_frame_host() |
| ->last_committed_common_params_has_user_gesture()); |
| } |
| RenderFrameHostImpl* rfh_a = current_frame_host(); |
| |
| // 2) Navigate to B. A should be stored in the back-forward cache. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| EXPECT_TRUE(rfh_a->IsInBackForwardCache()); |
| EXPECT_FALSE(root->current_frame_host() |
| ->last_committed_common_params_has_user_gesture()); |
| |
| // 3) GoBack to A. RenderFrameHost of A should be restored from the |
| // back-forward cache, and "has_user_gesture" is set to false correctly. |
| // Note that since this is a back-forward cache restore we create the |
| // DidCommitProvisionalLoadParams completely in the browser, so we got the |
| // correct value from the latest navigation. However, we did not update the |
| // renderer's navigation-related values, so the renderer's DocumentLoader |
| // still thinks the last "gesture" value is "true", which will get corrected |
| // on the next navigation. |
| { |
| FrameNavigateParamsCapturer params_capturer(root); |
| controller.GoBack(); |
| params_capturer.Wait(); |
| EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); |
| EXPECT_EQ(rfh_a, current_frame_host()); |
| // The navigation doesn't have user gesture. |
| EXPECT_FALSE(params_capturer.has_user_gesture()); |
| EXPECT_FALSE(root->current_frame_host() |
| ->last_committed_common_params_has_user_gesture()); |
| } |
| |
| // 4) Same-document navigation to A#foo without user gesture. At this point |
| // we will update the renderer's DocumentLoader's latest gesture value to |
| // "no user gesture", and we'll get the correct gesture value in |
| // DidCommitProvisionalLoadParams. |
| { |
| FrameNavigateParamsCapturer params_capturer(root); |
| EXPECT_TRUE( |
| NavigateToURLFromRendererWithoutUserGesture(shell(), url_a_foo)); |
| params_capturer.Wait(); |
| // The navigation doesn't have user gesture. |
| EXPECT_FALSE(params_capturer.has_user_gesture()); |
| EXPECT_FALSE(root->current_frame_host() |
| ->last_committed_common_params_has_user_gesture()); |
| } |
| } |
| |
| testing::Matcher<BackForwardCacheCanStoreTreeResult> MatchesTreeResult( |
| testing::Matcher<bool> same_origin, |
| GURL url) { |
| return testing::AllOf( |
| testing::Property("IsSameOrigin", |
| &BackForwardCacheCanStoreTreeResult::IsSameOrigin, |
| same_origin), |
| testing::Property("GetUrl", &BackForwardCacheCanStoreTreeResult::GetUrl, |
| url)); |
| } |
| |
| RenderFrameHostImpl* ChildFrame(RenderFrameHostImpl* rfh, int child_index) { |
| return rfh->child_at(child_index)->current_frame_host(); |
| } |
| |
| // Verifies that the reasons match those given and no others. |
| testing::Matcher<BackForwardCacheCanStoreDocumentResult> |
| BackForwardCacheBrowserTest::MatchesDocumentResult( |
| testing::Matcher<NotRestoredReasons> not_stored, |
| BlockListedFeatures block_listed) { |
| return testing::AllOf( |
| testing::Property( |
| "not_restored_reasons", |
| &BackForwardCacheCanStoreDocumentResult::not_restored_reasons, |
| not_stored), |
| testing::Property( |
| "blocklisted_features", |
| &BackForwardCacheCanStoreDocumentResult::blocklisted_features, |
| block_listed), |
| testing::Property( |
| "disabled_reasons", |
| &BackForwardCacheCanStoreDocumentResult::disabled_reasons, |
| BackForwardCacheCanStoreDocumentResult::DisabledReasonsMap()), |
| testing::Property( |
| "disallow_activation_reasons", |
| &BackForwardCacheCanStoreDocumentResult::disallow_activation_reasons, |
| std::set<uint64_t>())); |
| } |
| |
| // Check the contents of the BackForwardCacheCanStoreTreeResult of a page. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, TreeResultFeatureUsage) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(a, b, c)")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // 1) Navigate to a(a, b, c). |
| ASSERT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImplWrapper rfh(current_frame_host()); |
| |
| // 2) Add a blocking feature to the main frame A and the sub frame B. |
| current_frame_host() |
| ->UseDummyStickyBackForwardCacheDisablingFeatureForTesting(); |
| current_frame_host() |
| ->child_at(1) |
| ->current_frame_host() |
| ->UseDummyStickyBackForwardCacheDisablingFeatureForTesting(); |
| |
| GURL url_subframe_a = ChildFrame(rfh.get(), 0)->GetLastCommittedURL(); |
| GURL url_subframe_b = ChildFrame(rfh.get(), 1)->GetLastCommittedURL(); |
| GURL url_subframe_c = ChildFrame(rfh.get(), 2)->GetLastCommittedURL(); |
| |
| // 3) Initialize the reasons tree and navigate away to ensure that everything |
| // from the old frame has been destroyed. |
| BackForwardCacheCanStoreDocumentResultWithTree can_store_result = |
| web_contents() |
| ->GetController() |
| .GetBackForwardCache() |
| .GetCurrentBackForwardCacheEligibility(rfh.get()); |
| ASSERT_TRUE(NavigateToURL(shell(), url_b)); |
| ASSERT_TRUE(rfh.WaitUntilRenderFrameDeleted()); |
| |
| // 4) Check IsSameOrigin() and GetUrl(). |
| // a |
| EXPECT_THAT(*can_store_result.tree_reasons, |
| MatchesTreeResult(/*same_origin=*/true, |
| /*url=*/url_a)); |
| // a->a |
| EXPECT_THAT(*can_store_result.tree_reasons->GetChildren().at(0), |
| MatchesTreeResult(/*same_origin=*/true, |
| /*url=*/url_subframe_a)); |
| // a->b |
| EXPECT_THAT(*can_store_result.tree_reasons->GetChildren().at(1), |
| MatchesTreeResult(/*same_origin=*/false, |
| /*url=*/url_subframe_b)); |
| // a->c |
| EXPECT_THAT(*can_store_result.tree_reasons->GetChildren().at(2), |
| MatchesTreeResult(/*same_origin=*/false, |
| /*url=*/url_subframe_c)); |
| |
| // 5) Check that the blocking reasons match. |
| // a |
| EXPECT_THAT(can_store_result.tree_reasons->GetDocumentResult(), |
| MatchesDocumentResult( |
| NotRestoredReasons({NotRestoredReason::kBlocklistedFeatures}), |
| BlockListedFeatures( |
| {blink::scheduler::WebSchedulerTrackedFeature::kDummy}))); |
| // a->a |
| EXPECT_THAT( |
| can_store_result.tree_reasons->GetChildren().at(0)->GetDocumentResult(), |
| MatchesDocumentResult(NotRestoredReasons(), |
| BlockListedFeatures(BlockListedFeatures()))); |
| // a->b |
| EXPECT_THAT( |
| can_store_result.tree_reasons->GetChildren().at(1)->GetDocumentResult(), |
| MatchesDocumentResult( |
| NotRestoredReasons({NotRestoredReason::kBlocklistedFeatures}), |
| BlockListedFeatures( |
| {blink::scheduler::WebSchedulerTrackedFeature::kDummy}))); |
| // a->c |
| EXPECT_THAT( |
| can_store_result.tree_reasons->GetChildren().at(2)->GetDocumentResult(), |
| MatchesDocumentResult(NotRestoredReasons(), |
| BlockListedFeatures(BlockListedFeatures()))); |
| } |
| |
| // Check the contents of the BackForwardCacheCanStoreTreeResult of a page when |
| // it is evicted. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| TreeResultEvictionMainFrame) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // 1) Navigate to a. |
| ASSERT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImplWrapper rfh_a(current_frame_host()); |
| rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this); |
| |
| // 2) Navigate to B and evict A by JavaScript execution. |
| ASSERT_TRUE(NavigateToURL(shell(), url_b)); |
| EvictByJavaScript(rfh_a.get()); |
| ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted()); |
| |
| // 3) Go back to A. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| ExpectNotRestored({NotRestoredReason::kJavaScriptExecution}, {}, {}, {}, {}, |
| FROM_HERE); |
| EXPECT_THAT(GetTreeResult()->GetDocumentResult(), |
| MatchesDocumentResult( |
| NotRestoredReasons({NotRestoredReason::kJavaScriptExecution}), |
| BlockListedFeatures())); |
| } |
| |
| // Check the contents of the BackForwardCacheCanStoreTreeResult of a page when |
| // its subframe is evicted. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| TreeResultEvictionSubFrame) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| GURL url_c(embedded_test_server()->GetURL("c.com", "/title1.html")); |
| |
| // 1) Navigate to A. |
| ASSERT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImplWrapper rfh_a(current_frame_host()); |
| RenderFrameHostImplWrapper rfh_b( |
| current_frame_host()->child_at(0)->current_frame_host()); |
| rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this); |
| |
| // 2) Navigate to C and evict A's subframe B by JavaScript execution. |
| ASSERT_TRUE(NavigateToURL(shell(), url_c)); |
| EvictByJavaScript(rfh_b.get()); |
| ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted()); |
| |
| // 3) Go back to A. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| ExpectNotRestored({NotRestoredReason::kJavaScriptExecution}, {}, {}, {}, {}, |
| FROM_HERE); |
| // Main frame result in the tree is empty. |
| EXPECT_THAT( |
| GetTreeResult()->GetDocumentResult(), |
| MatchesDocumentResult(NotRestoredReasons(), BlockListedFeatures())); |
| // Subframe result in the tree contains the reason. |
| EXPECT_THAT(GetTreeResult()->GetChildren().at(0)->GetDocumentResult(), |
| MatchesDocumentResult( |
| NotRestoredReasons({NotRestoredReason::kJavaScriptExecution}), |
| BlockListedFeatures())); |
| } |
| |
| // Check the contents of the BackForwardCacheCanStoreTreeResult of a page when |
| // its subframe's subframe is evicted. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| TreeResultEvictionSubFramesSubframe) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b(c))")); |
| GURL url_d(embedded_test_server()->GetURL("d.com", "/title1.html")); |
| |
| // 1) Navigate to A. |
| ASSERT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImplWrapper rfh_a(current_frame_host()); |
| RenderFrameHostImplWrapper rfh_c(current_frame_host() |
| ->child_at(0) |
| ->current_frame_host() |
| ->child_at(0) |
| ->current_frame_host()); |
| rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this); |
| |
| // 2) Navigate to D and evict C by JavaScript execution. |
| ASSERT_TRUE(NavigateToURL(shell(), url_d)); |
| EvictByJavaScript(rfh_c.get()); |
| ASSERT_TRUE(rfh_a.WaitUntilRenderFrameDeleted()); |
| |
| // 3) Go back to A. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| ExpectNotRestored({NotRestoredReason::kJavaScriptExecution}, {}, {}, {}, {}, |
| FROM_HERE); |
| // Main frame result in the tree is empty. |
| EXPECT_THAT( |
| GetTreeResult()->GetDocumentResult(), |
| MatchesDocumentResult(NotRestoredReasons(), BlockListedFeatures())); |
| // The first level subframe result in the tree is empty. |
| EXPECT_THAT( |
| GetTreeResult()->GetChildren().at(0)->GetDocumentResult(), |
| MatchesDocumentResult(NotRestoredReasons(), BlockListedFeatures())); |
| // The second level subframe result in the tree contains the reason. |
| EXPECT_THAT(GetTreeResult() |
| ->GetChildren() |
| .at(0) |
| ->GetChildren() |
| .at(0) |
| ->GetDocumentResult(), |
| MatchesDocumentResult( |
| NotRestoredReasons({NotRestoredReason::kJavaScriptExecution}), |
| BlockListedFeatures())); |
| } |
| |
| void BackForwardCacheBrowserTest::InstallUnloadHandlerOnMainFrame() { |
| EXPECT_TRUE(ExecJs(current_frame_host(), R"( |
| localStorage["unload_run_count"] = 0; |
| window.onunload = () => { |
| localStorage["unload_run_count"] = |
| 1 + parseInt(localStorage["unload_run_count"]); |
| }; |
| )")); |
| EXPECT_EQ("0", GetUnloadRunCount()); |
| } |
| |
| void BackForwardCacheBrowserTest::InstallUnloadHandlerOnSubFrame() { |
| TestNavigationObserver navigation_observer(shell()->web_contents(), 1); |
| EXPECT_TRUE(ExecJs(current_frame_host(), R"( |
| const iframeElement = document.createElement("iframe"); |
| iframeElement.src = "%s"; |
| document.body.appendChild(iframeElement); |
| )")); |
| navigation_observer.Wait(); |
| RenderFrameHostImpl* subframe_render_frame_host = |
| current_frame_host()->child_at(0)->current_frame_host(); |
| EXPECT_TRUE(ExecJs(subframe_render_frame_host, R"( |
| localStorage["unload_run_count"] = 0; |
| window.onunload = () => { |
| localStorage["unload_run_count"] = |
| 1 + parseInt(localStorage["unload_run_count"]); |
| }; |
| )")); |
| EXPECT_EQ("0", GetUnloadRunCount()); |
| } |
| |
| EvalJsResult BackForwardCacheBrowserTest::GetUnloadRunCount() { |
| return GetLocalStorage(current_frame_host(), "unload_run_count"); |
| } |
| |
| bool BackForwardCacheBrowserTest::AddBlocklistedFeature(RenderFrameHost* rfh) { |
| // Add kDummy as blocking feature. |
| RenderFrameHostImplWrapper rfh_a(rfh); |
| rfh_a->UseDummyStickyBackForwardCacheDisablingFeatureForTesting(); |
| return true; |
| } |
| |
| void BackForwardCacheBrowserTest::ExpectNotRestoredDueToBlocklistedFeature( |
| base::Location location) { |
| ExpectNotRestored({NotRestoredReason::kBlocklistedFeatures}, |
| {blink::scheduler::WebSchedulerTrackedFeature::kDummy}, {}, |
| {}, {}, location); |
| } |
| |
| const ukm::TestAutoSetUkmRecorder& BackForwardCacheBrowserTest::ukm_recorder() { |
| return *ukm_recorder_; |
| } |
| |
| const base::HistogramTester& BackForwardCacheBrowserTest::histogram_tester() { |
| return *histogram_tester_; |
| } |
| |
| // Ensure that psges with unload are only allowed to enter back/forward cache by |
| // default on Android. |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, UnloadAllowedFlag) { |
| #if BUILDFLAG(IS_ANDROID) |
| ASSERT_TRUE(BackForwardCacheImpl::IsUnloadAllowed()); |
| #else |
| ASSERT_FALSE(BackForwardCacheImpl::IsUnloadAllowed()); |
| #endif |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| FrameWithBlocklistedFeatureNotCached) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // Navigate to a page that contains a blocklisted feature. |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.com", "/title1.html"))); |
| |
| RenderFrameHostWrapper rfh(current_frame_host()); |
| |
| ASSERT_TRUE(AddBlocklistedFeature(rfh.get())); |
| |
| // Navigate away. |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("b.com", "/title1.html"))); |
| |
| // The page with the unsupported feature should be deleted (not cached). |
| ASSERT_TRUE(rfh.WaitUntilRenderFrameDeleted()); |
| |
| // Go back. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| ExpectNotRestoredDueToBlocklistedFeature(FROM_HERE); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| SubframeWithBlocklistedFeatureNotCached) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // Navigate to a page with an iframe that contains a blocklisted feature. |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)"))); |
| |
| RenderFrameHostWrapper rfh( |
| current_frame_host()->child_at(0)->current_frame_host()); |
| |
| ASSERT_TRUE(AddBlocklistedFeature(rfh.get())); |
| |
| // Navigate away. |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("b.com", "/title1.html"))); |
| |
| // The page with the unsupported feature should be deleted (not cached). |
| ASSERT_TRUE(rfh.WaitUntilRenderFrameDeleted()); |
| |
| // Go back. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| ExpectNotRestoredDueToBlocklistedFeature(FROM_HERE); |
| } |
| |
| class BackForwardCacheBrowserUnloadHandlerTest |
| : public BackForwardCacheBrowserTest, |
| public ::testing::WithParamInterface< |
| std::tuple<bool, bool, bool, TestFrameType>> { |
| public: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| if (IsUnloadAllowed()) { |
| EnableFeatureAndSetParams(kBackForwardCacheUnloadAllowed, "", ""); |
| } else { |
| DisableFeature(kBackForwardCacheUnloadAllowed); |
| } |
| if (IsUnloadBlocklisted()) { |
| EnableFeatureAndSetParams(blink::features::kUnloadBlocklisted, "", ""); |
| } else { |
| DisableFeature(blink::features::kUnloadBlocklisted); |
| } |
| if (IsUnloadDeprecationOptedOut()) { |
| EnableFeatureAndSetParams(blink::features::kDeprecateUnloadOptOut, "", |
| ""); |
| } else { |
| DisableFeature(blink::features::kDeprecateUnloadOptOut); |
| } |
| |
| BackForwardCacheBrowserTest::SetUpCommandLine(command_line); |
| } |
| |
| bool IsUnloadAllowed() { return std::get<0>(GetParam()); } |
| bool IsUnloadBlocklisted() { return std::get<1>(GetParam()); } |
| bool IsUnloadDeprecationOptedOut() { return std::get<2>(GetParam()); } |
| |
| TestFrameType GetTestFrameType() { return std::get<3>(GetParam()); } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Ensure that unload handlers in main frames and subframes block caching, |
| // depending on unload deprecation status and OS. |
| IN_PROC_BROWSER_TEST_P(BackForwardCacheBrowserUnloadHandlerTest, |
| UnloadHandlerPresent) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| |
| // 1) Navigate to A. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| |
| BackForwardCacheCanStoreDocumentResult::NotRestoredReasons |
| expected_blocking_reasons; |
| std::vector<blink::scheduler::WebSchedulerTrackedFeature> |
| expected_blocklisted_reason; |
| if (IsUnloadBlocklisted()) { |
| expected_blocking_reasons.Put( |
| BackForwardCacheMetrics::NotRestoredReason::kBlocklistedFeatures); |
| expected_blocklisted_reason.push_back( |
| blink::scheduler::WebSchedulerTrackedFeature::kUnloadHandler); |
| } |
| switch (GetTestFrameType()) { |
| case content::TestFrameType::kMainFrame: |
| InstallUnloadHandlerOnMainFrame(); |
| expected_blocking_reasons.Put(BackForwardCacheMetrics::NotRestoredReason:: |
| kUnloadHandlerExistsInMainFrame); |
| break; |
| case content::TestFrameType::kSubFrame: |
| InstallUnloadHandlerOnSubFrame(); |
| expected_blocking_reasons.Put(BackForwardCacheMetrics::NotRestoredReason:: |
| kUnloadHandlerExistsInSubFrame); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| |
| // 2) Navigate to B. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| |
| // 3) Go back. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| |
| bool unload_never_blocks = IsUnloadAllowed(); |
| bool unload_deprecated_and_not_opted_out = |
| (base::FeatureList::IsEnabled(network::features::kDeprecateUnload) && |
| !IsUnloadDeprecationOptedOut()); |
| if (unload_never_blocks || unload_deprecated_and_not_opted_out) { |
| // Pages with unload handlers are eligible for bfcache only if it is |
| // specifically allowed (happens on Android). Also, when unload is |
| // deprecated and `kDeprecateUnloadOptOut` doesn't override it, unload |
| // handlers cannot be installed so there should be no blocker for BFCache. |
| ExpectRestored(FROM_HERE); |
| EXPECT_EQ("0", GetUnloadRunCount()); |
| } else { |
| ExpectNotRestored(expected_blocking_reasons, expected_blocklisted_reason, |
| {}, {}, {}, FROM_HERE); |
| EXPECT_EQ("1", GetUnloadRunCount()); |
| } |
| } |
| |
| // First param: whether unload is allowed or not. |
| // Second one: whether unload is blocklisted or not. |
| // Third one: whether it's opted out from unload deprecation or not. |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| BackForwardCacheBrowserUnloadHandlerTest, |
| ::testing::Combine(::testing::Bool(), |
| ::testing::Bool(), |
| ::testing::Bool(), |
| ::testing::Values(TestFrameType::kMainFrame, |
| TestFrameType::kSubFrame))); |
| |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, DisableForRenderFrameHost) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| |
| // 1) Navigate to A. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostWrapper rfh_wrapper_a(current_frame_host()); |
| |
| // 2) Navigate to B. |
| EXPECT_TRUE(NavigateToURL(shell(), url_b)); |
| RenderFrameHostWrapper rfh_wrapper_b(current_frame_host()); |
| |
| // Regardless of whether the source Id is set or not, it shouldn't affect the |
| // result of the BFCache eviction. |
| BackForwardCache::DisabledReason test_reason = |
| BackForwardCacheDisable::DisabledReason( |
| BackForwardCacheDisable::DisabledReasonId::kUnknown); |
| |
| // 3) Disable BFCache for A with UKM source Id and go back. |
| BackForwardCache::DisableForRenderFrameHost( |
| rfh_wrapper_a.get(), test_reason, ukm::UkmRecorder::GetNewSourceID()); |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| ASSERT_TRUE(rfh_wrapper_a.WaitUntilRenderFrameDeleted()); |
| // Page A should be evicted properly. |
| ExpectNotRestored({BackForwardCacheMetrics::NotRestoredReason:: |
| kDisableForRenderFrameHostCalled}, |
| {}, {}, {test_reason}, {}, FROM_HERE); |
| |
| // 4) Disable BFCache for B without UKM source Id and go forward. |
| BackForwardCache::DisableForRenderFrameHost(rfh_wrapper_b.get(), test_reason); |
| ASSERT_TRUE(HistoryGoForward(web_contents())); |
| ASSERT_TRUE(rfh_wrapper_b.WaitUntilRenderFrameDeleted()); |
| // Page B should be evicted properly. |
| ExpectNotRestored({BackForwardCacheMetrics::NotRestoredReason:: |
| kDisableForRenderFrameHostCalled}, |
| {}, {}, {test_reason}, {}, FROM_HERE); |
| } |
| |
| namespace { |
| enum class SubframeType { SameSite, CrossSite }; |
| } |
| |
| class BackForwardCacheEvictionDueToSubframeNavigationBrowserTest |
| : public BackForwardCacheBrowserTest, |
| public ::testing::WithParamInterface<SubframeType> { |
| public: |
| // Provides meaningful param names instead of /0 and /1. |
| static std::string DescribeParams( |
| const ::testing::TestParamInfo<ParamType>& info) { |
| switch (info.param) { |
| case SubframeType::SameSite: |
| return "SameSite"; |
| case SubframeType::CrossSite: |
| return "CrossSite"; |
| } |
| } |
| |
| protected: |
| bool UseCrossOriginSubframe() const { |
| return GetParam() == SubframeType::CrossSite; |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_P( |
| BackForwardCacheEvictionDueToSubframeNavigationBrowserTest, |
| SubframePendingCommitShouldPreventCache) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL a_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL subframe_url = embedded_test_server()->GetURL( |
| UseCrossOriginSubframe() ? "b.com" : "a.com", "/title1.html"); |
| |
| IsolateOriginsForTesting(embedded_test_server(), web_contents(), |
| std::vector<std::string>{"a.com", "b.com"}); |
| |
| // 1) Navigate to a.com. |
| EXPECT_TRUE(NavigateToURL(shell(), a_url)); |
| RenderFrameHostImpl* main_frame = current_frame_host(); |
| |
| // 2) Add subframe and wait for empty document to commit. |
| CreateSubframe(web_contents(), "child", GURL(""), true); |
| |
| CommitMessageDelayer commit_message_delayer( |
| web_contents(), subframe_url, |
| base::BindLambdaForTesting([&](RenderFrameHost*) { |
| // 5) Test that page cannot be stored in bfcache when subframe is |
| // pending commit. |
| BackForwardCacheCanStoreDocumentResultWithTree can_store_result = |
| web_contents() |
| ->GetController() |
| .GetBackForwardCache() |
| .GetCurrentBackForwardCacheEligibility( |
| static_cast<RenderFrameHostImpl*>(main_frame)); |
| EXPECT_TRUE(can_store_result.flattened_reasons.HasNotRestoredReason( |
| BackForwardCacheMetrics::NotRestoredReason::kSubframeIsNavigating)); |
| })); |
| |
| // 3) Start navigation in subframe to |subframe_url|. |
| ExecuteScriptAsync( |
| main_frame, |
| JsReplace("document.querySelector('#child').src = $1;", subframe_url)); |
| // 4) Wait until subframe navigation is pending commit. |
| commit_message_delayer.Wait(); |
| } |
| |
| // Check that when the main frame gets BFCached while the subframe navigation |
| // deferring NavigationThrottles has already ran, the issue that the subframe |
| // navigation escapes the throttle deferral is addressed. |
| IN_PROC_BROWSER_TEST_P( |
| BackForwardCacheEvictionDueToSubframeNavigationBrowserTest, |
| MainFrameCommitFirstAndSubframePendingCommitShouldBeEvicted) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| // Prepare the main frame and the sub frame, where both of them are same-site. |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(a)")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| FrameTreeNode* root = web_contents()->GetPrimaryFrameTree().root(); |
| FrameTreeNode* child = root->child_at(0); |
| RenderFrameHostImplWrapper child_rfh(child->current_frame_host()); |
| |
| // Navigate both frames simultaneously. |
| const std::string subframe_origin = |
| UseCrossOriginSubframe() ? "b.com" : "a.com"; |
| GURL new_url_1( |
| embedded_test_server()->GetURL(subframe_origin, "/title1.html")); |
| GURL new_url_2( |
| embedded_test_server()->GetURL(subframe_origin, "/title2.html")); |
| TestNavigationManager manager1(web_contents(), new_url_1); |
| TestNavigationManager manager2(web_contents(), new_url_2); |
| auto script = JsReplace("location = $1; frames[0].location = $2;", new_url_1, |
| new_url_2); |
| EXPECT_TRUE(ExecJs(web_contents(), script)); |
| |
| // Wait for main frame request, but don't commit it yet. This should create |
| // a speculative RenderFrameHost. |
| ASSERT_TRUE(manager1.WaitForRequestStart()); |
| |
| // Wait for subframe request, but don't commit it yet. |
| ASSERT_TRUE(manager2.WaitForRequestStart()); |
| |
| // Now let the main frame commit. |
| ASSERT_TRUE(manager1.WaitForNavigationFinished()); |
| // Make sure the main frame is at the new URL. |
| ASSERT_TRUE(root->current_frame_host()->IsRenderFrameLive()); |
| ASSERT_EQ(new_url_1, root->current_frame_host()->GetLastCommittedURL()); |
| |
| // The subframe should be gone now: it should have been evicted from BFCache |
| // because the subframe is still navigating; otherwise, this will cause a |
| // crash. |
| ASSERT_TRUE(manager2.WaitForNavigationFinished()); |
| EXPECT_TRUE(child_rfh.IsDestroyed()); |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| ExpectNotRestored( |
| {BackForwardCacheMetrics::NotRestoredReason::kSubframeIsNavigating}, {}, |
| {}, {}, {}, FROM_HERE); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| BackForwardCacheEvictionDueToSubframeNavigationBrowserTest, |
| ::testing::Values(SubframeType::SameSite, SubframeType::CrossSite), |
| &BackForwardCacheEvictionDueToSubframeNavigationBrowserTest:: |
| DescribeParams); |
| |
| namespace { |
| enum class SubframeNavigationType { WithoutURLLoader, WithURLLoader }; |
| } |
| |
| // Test for pages which has subframe(s) with ongoing navigation(s). |
| class BackForwardCacheWithSubframeNavigationBrowserTest |
| : public BackForwardCacheBrowserTest { |
| protected: |
| void SetUpOnMainThread() override { |
| BackForwardCacheBrowserTest::SetUpOnMainThread(); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| EnableFeatureAndSetParams(features::kBackForwardCache, "cache_size", |
| base::NumberToString(2)); |
| BackForwardCacheBrowserTest::SetUpCommandLine(command_line); |
| } |
| |
| // Start a subframe navigation and pause it when we get the confirmation |
| // dialog triggered by beforeunload event, which is before |
| // WillCommitWithoutUrlLoader or WillStartRequest. |
| void NavigateSubframeAndPauseAtBeforeUnload( |
| BeforeUnloadBlockingDelegate& beforeunload_pauser, |
| RenderFrameHostImpl* sub_rfh, |
| const GURL& subframe_navigate_url, |
| std::string_view iframe_id) { |
| ASSERT_TRUE(ExecJs(sub_rfh, R"( |
| window.addEventListener('beforeunload', e => |
| e.returnValue='blocked' |
| );)")); |
| |
| // Start a subframe navigation which will trigger the beforeunload dialog |
| // that pauses that navigation. Using `BeginNavigateIframeToURL` is |
| // necessary here, since we pause this navigation on beforeunload event. So, |
| // we don't want to wait for the navigation to finish. |
| BeginNavigateIframeToURL(web_contents(), iframe_id, subframe_navigate_url); |
| beforeunload_pauser.Wait(); |
| } |
| |
| // Start a subframe navigation and pause it before `DidCommitNavigation`. |
| void NavigateSubframeAndPauseAtDidCommit(FrameTreeNode* ftn, |
| const GURL& subframe_navigate_url) { |
| // Enforce the creation of speculative RFH to correctly wait for |
| // the commit event. |
| SpeculativeRenderFrameHostObserver observer(web_contents(), |
| subframe_navigate_url); |
| // We have to pause a navigation before `DidCommitNavigation`, so we don't |
| // want to wait for the navigation to finish. |
| ASSERT_TRUE(BeginNavigateToURLFromRenderer(ftn, subframe_navigate_url)); |
| // Navigation without a URL loader shall always create the speculative RFH |
| // immediately or never create one. |
| // The same-site navigation to the subframe will not create a new |
| // speculative RFH if render document is not enabled for subframes. |
| if (subframe_navigate_url.SchemeIsHTTPOrHTTPS() && |
| ShouldCreateNewRenderFrameHostOnSameSiteNavigation( |
| /*is_main_frame=*/false, /*is_local_root=*/true)) { |
| observer.Wait(); |
| } |
| |
| // Wait until the navigation is pending commit. Note that the navigation |
| // might use a speculative RenderFrameHost, so use that if necessary. |
| RenderFrameHostImpl* speculative_rfh = |
| ftn->render_manager()->speculative_frame_host(); |
| CommitNavigationPauser commit_pauser( |
| speculative_rfh ? speculative_rfh : ftn->current_frame_host()); |
| commit_pauser.WaitForCommitAndPause(); |
| } |
| |
| // Put a page which has a subframe with a navigation which hasn't reached the |
| // "pending commit" stage nor sent a network request into BackForwardCache and |
| // confirm the subframe navigation has been deferred. |
| void BFCachePageWithSubframeNavigationBeforeDidStartNavigation( |
| const GURL& main_frame_navigate_url, |
| const GURL& subframe_navigate_url, |
| RenderFrameHostImplWrapper& sub_rfh, |
| TestNavigationManager& subframe_navigation_manager, |
| std::string_view iframe_id) { |
| FrameTreeNode* child_ftn = |
| web_contents()->GetPrimaryFrameTree().root()->child_at(0); |
| { |
| BeforeUnloadBlockingDelegate beforeunload_pauser(web_contents()); |
| NavigateSubframeAndPauseAtBeforeUnload(beforeunload_pauser, sub_rfh.get(), |
| subframe_navigate_url, iframe_id); |
| |
| // Subframe navigation is ongoing, so `NavigateToURL` cannot be used since |
| // this function waits for all frames including subframe to finish |
| // loading. |
| ASSERT_TRUE(NavigateToURLFromRenderer(sub_rfh->GetMainFrame(), |
| main_frame_navigate_url)); |
| |
| // The subframe navigation hasn't reached the "pending commit" stage nor |
| // sent a network request, so the page is eligible for BackForwardCache. |
| EXPECT_TRUE(sub_rfh->GetMainFrame()->IsInBackForwardCache()); |
| EXPECT_TRUE(sub_rfh->IsInBackForwardCache()); |
| } |
| web_contents()->SetDelegate(shell()); |
| |
| // Wait until the subframe navigation is deferred. |
| ASSERT_TRUE( |
| subframe_navigation_manager.WaitForFirstYieldAfterDidStartNavigation()); |
| NavigationRequest* child_navigation = child_ftn->navigation_request(); |
| ASSERT_NE(child_navigation, nullptr); |
| EXPECT_TRUE(child_navigation->IsDeferredForTesting()); |
| } |
| }; |
| |
| class BackForwardCacheWithSubframeNavigationWithParamBrowserTest |
| : public BackForwardCacheWithSubframeNavigationBrowserTest, |
| public ::testing::WithParamInterface<SubframeNavigationType> { |
| public: |
| // Provides meaningful param names instead of /0 and /1. |
| static std::string DescribeParams( |
| const ::testing::TestParamInfo<ParamType>& info) { |
| switch (info.param) { |
| case SubframeNavigationType::WithoutURLLoader: |
| return "WithoutURLLoader"; |
| case SubframeNavigationType::WithURLLoader: |
| return "WithURLLoader"; |
| } |
| } |
| }; |
| |
| // Confirm that BackForwardCache is blocked when there is only 1 navigation and |
| // it's pending commit. |
| IN_PROC_BROWSER_TEST_P( |
| BackForwardCacheWithSubframeNavigationWithParamBrowserTest, |
| SubframeNavigationWithPendingCommitShouldPreventCache) { |
| const GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| const GURL subframe_url = embedded_test_server()->GetURL( |
| "b.com", "/cross_site_iframe_factory.html?b()"); |
| const GURL navigate_url( |
| embedded_test_server()->GetURL("c.com", "/title1.html")); |
| const GURL subframe_navigate_url = |
| GetParam() == SubframeNavigationType::WithURLLoader |
| ? embedded_test_server()->GetURL("b.com", "/title1.html") |
| : GURL("about:blank"); |
| |
| // Navigate to a page with a cross site iframe. |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); |
| |
| RenderFrameHostImplWrapper main_rfh(current_frame_host()); |
| FrameTreeNode* child_node = main_rfh.get()->child_at(0); |
| RenderFrameHostImplWrapper sub_rfh(child_node->current_frame_host()); |
| |
| // Pause subframe's navigation before `DidCommitNavigation`. |
| NavigateSubframeAndPauseAtDidCommit(child_node, subframe_navigate_url); |
| |
| // Subframe navigation is ongoing, so `NavigateToURL` cannot be used since |
| // this function waits for all frames including subframe to finish loading. |
| ASSERT_TRUE(NavigateToURLFromRenderer(main_rfh.get(), navigate_url)); |
| |
| // Subframe navigation has reached the "pending commit" stage, so the page is |
| // not eligible for BackForwardCache. |
| EXPECT_TRUE(main_rfh.WaitUntilRenderFrameDeleted()); |
| EXPECT_TRUE(sub_rfh.WaitUntilRenderFrameDeleted()); |
| |
| // Navigate back. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| ExpectNotRestored({NotRestoredReason::kSubframeIsNavigating}, {}, {}, {}, {}, |
| FROM_HERE); |
| |
| // Confirm that subframe's url didn't change. |
| EXPECT_EQ(subframe_url, current_frame_host()->child_at(0)->current_url()); |
| } |
| |
| // Confirm that BackForwardCache is blocked when there are 2 navigations, 1 not |
| // pending commit yet, and 1 pending commit. |
| IN_PROC_BROWSER_TEST_F( |
| BackForwardCacheWithSubframeNavigationBrowserTest, |
| MultipleSubframeNavigationWithBeforeAndPendingCommitShouldPreventCache) { |
| // This test relies on the main frame and the iframe to live in different |
| // processes. This allows one renderer process to proceed a navigation while |
| // the other renderer process is busy executing its beforeunload handler. |
| if (!AreAllSitesIsolatedForTesting()) { |
| GTEST_SKIP() << "Site isolation is not enabled!"; |
| } |
| const GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b,c)")); |
| const GURL subframe_b_url = embedded_test_server()->GetURL( |
| "b.com", "/cross_site_iframe_factory.html?b()"); |
| const GURL subframe_c_url = embedded_test_server()->GetURL( |
| "c.com", "/cross_site_iframe_factory.html?c()"); |
| const GURL navigate_url( |
| embedded_test_server()->GetURL("d.com", "/title1.html")); |
| const GURL subframe_navigate_url = GURL("about:blank"); |
| |
| // Navigate to a page with two cross site iframes. |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImplWrapper main_rfh(current_frame_host()); |
| RenderFrameHostImplWrapper sub_rfh_b( |
| main_rfh.get()->child_at(0)->current_frame_host()); |
| RenderFrameHostImplWrapper sub_rfh_c( |
| main_rfh.get()->child_at(1)->current_frame_host()); |
| |
| { |
| // The subframe_b itself does have a dialog-showing beforeunload handler. |
| // Pause subframe_b's navigation when we get the confirmation dialog |
| // triggered by beforeunload event. |
| BeforeUnloadBlockingDelegate beforeunload_pauser(web_contents()); |
| NavigateSubframeAndPauseAtBeforeUnload(beforeunload_pauser, sub_rfh_b.get(), |
| subframe_navigate_url, |
| /*iframe_id=*/"child-0"); |
| |
| // Pause subframe_c's navigation before `DidCommitNavigation`. |
| NavigateSubframeAndPauseAtDidCommit(main_rfh.get()->child_at(1), |
| subframe_navigate_url); |
| |
| // Subframe navigation is ongoing, so `NavigateToURL` cannot be used since |
| // this function waits for all frames including subframe to finish loading. |
| ASSERT_TRUE(NavigateToURLFromRenderer(main_rfh.get(), navigate_url)); |
| |
| // The subframe_c's navigation already started committing, so the page is |
| // not eligible for BackForwardCache. |
| EXPECT_TRUE(main_rfh.WaitUntilRenderFrameDeleted()); |
| EXPECT_TRUE(sub_rfh_b.WaitUntilRenderFrameDeleted()); |
| EXPECT_TRUE(sub_rfh_c.WaitUntilRenderFrameDeleted()); |
| } |
| web_contents()->SetDelegate(shell()); |
| |
| // Navigate back. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| ExpectNotRestored({NotRestoredReason::kSubframeIsNavigating}, {}, {}, {}, {}, |
| FROM_HERE); |
| |
| // Confirm that subframe's url didn't change. |
| EXPECT_EQ(subframe_b_url, current_frame_host()->child_at(0)->current_url()); |
| EXPECT_EQ(subframe_c_url, current_frame_host()->child_at(1)->current_url()); |
| } |
| |
| // Confirm that BackForwardCache is blocked when there are 2 navigations, 1 has |
| // not sent a network request yet, and 1 has already sent request. |
| IN_PROC_BROWSER_TEST_F( |
| BackForwardCacheWithSubframeNavigationBrowserTest, |
| MultipleSubframeNavigationWithBeforeAndAfterSendingRequestShouldPreventCache) { |
| // This test relies on the main frame and the iframe to live in different |
| // processes. This allows one renderer process to proceed a navigation while |
| // the other renderer process is busy executing its beforeunload handler. |
| if (!AreAllSitesIsolatedForTesting()) { |
| GTEST_SKIP() << "Site isolation is not enabled!"; |
| } |
| const GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b,c)")); |
| const GURL subframe_b_url = embedded_test_server()->GetURL( |
| "b.com", "/cross_site_iframe_factory.html?b()"); |
| const GURL subframe_c_url = embedded_test_server()->GetURL( |
| "c.com", "/cross_site_iframe_factory.html?c()"); |
| const GURL navigate_url( |
| embedded_test_server()->GetURL("d.com", "/title1.html")); |
| const GURL subframe_b_navigate_url( |
| embedded_test_server()->GetURL("b.com", "/title1.html")); |
| const GURL subframe_c_navigate_url( |
| embedded_test_server()->GetURL("c.com", "/title1.html")); |
| |
| // Navigate to a page with two cross site iframes. |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImplWrapper main_rfh(current_frame_host()); |
| RenderFrameHostImplWrapper sub_rfh_b( |
| main_rfh.get()->child_at(0)->current_frame_host()); |
| RenderFrameHostImplWrapper sub_rfh_c( |
| main_rfh.get()->child_at(1)->current_frame_host()); |
| |
| // Pause a subframe_b navigation on `WillStartRequest` before sending a |
| // network request. |
| TestNavigationManager subframe_b_navigation_manager(web_contents(), |
| subframe_b_navigate_url); |
| ASSERT_TRUE( |
| BeginNavigateToURLFromRenderer(sub_rfh_b.get(), subframe_b_navigate_url)); |
| ASSERT_TRUE(subframe_b_navigation_manager.WaitForRequestStart()); |
| |
| // Pause a subframe_c navigation on `WillProcessResponse` after sending a |
| // network request. |
| TestNavigationManager subframe_c_navigation_manager(web_contents(), |
| subframe_c_navigate_url); |
| ASSERT_TRUE( |
| BeginNavigateToURLFromRenderer(sub_rfh_c.get(), subframe_c_navigate_url)); |
| ASSERT_TRUE(subframe_c_navigation_manager.WaitForResponse()); |
| |
| // Subframe navigation is ongoing, so `NavigateToURL` cannot be used since |
| // this function waits for all frames including subframe to finish loading. |
| ASSERT_TRUE(NavigateToURLFromRenderer(main_rfh.get(), navigate_url)); |
| |
| // The subframe_c's navigation has already sent a network request, so the page |
| // is not eligible for BackForwardCache. |
| EXPECT_TRUE(main_rfh.WaitUntilRenderFrameDeleted()); |
| EXPECT_TRUE(sub_rfh_b.WaitUntilRenderFrameDeleted()); |
| EXPECT_TRUE(sub_rfh_c.WaitUntilRenderFrameDeleted()); |
| EXPECT_TRUE(subframe_b_navigation_manager.WaitForNavigationFinished()); |
| EXPECT_TRUE(subframe_c_navigation_manager.WaitForNavigationFinished()); |
| EXPECT_FALSE(subframe_b_navigation_manager.was_committed()); |
| EXPECT_FALSE(subframe_c_navigation_manager.was_committed()); |
| |
| // Navigate back. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| ExpectNotRestored({NotRestoredReason::kSubframeIsNavigating}, {}, {}, {}, {}, |
| FROM_HERE); |
| |
| // Confirm that subframe's url didn't change. |
| EXPECT_EQ(subframe_b_url, current_frame_host()->child_at(0)->current_url()); |
| EXPECT_EQ(subframe_c_url, current_frame_host()->child_at(1)->current_url()); |
| } |
| |
| // Confirm that subframe navigation which needs url loader that has already sent |
| // a network request should block BackForwardCache. |
| IN_PROC_BROWSER_TEST_F( |
| BackForwardCacheWithSubframeNavigationBrowserTest, |
| SubframeNavigationWithUrlLoaderAfterSendingRequestShouldPreventCache) { |
| const GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| const GURL subframe_url = embedded_test_server()->GetURL( |
| "b.com", "/cross_site_iframe_factory.html?b()"); |
| const GURL navigate_url( |
| embedded_test_server()->GetURL("c.com", "/title1.html")); |
| const GURL subframe_navigate_url( |
| embedded_test_server()->GetURL("b.com", "/title2.html")); |
| |
| // Navigate to a page with a cross site iframe. |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImplWrapper main_rfh(current_frame_host()); |
| RenderFrameHostImplWrapper sub_rfh( |
| main_rfh.get()->child_at(0)->current_frame_host()); |
| TestNavigationManager subframe_navigation_manager(web_contents(), |
| subframe_navigate_url); |
| ASSERT_TRUE( |
| BeginNavigateToURLFromRenderer(sub_rfh.get(), subframe_navigate_url)); |
| |
| // Pause the subframe navigation on `WillProcessResponse`. |
| ASSERT_TRUE(subframe_navigation_manager.WaitForResponse()); |
| |
| // Subframe navigation is ongoing, so `NavigateToURL` cannot be used since |
| // this function waits for all frames including subframe to finish loading. |
| ASSERT_TRUE(NavigateToURLFromRenderer(main_rfh.get(), navigate_url)); |
| |
| // Subframe navigation has already sent a network request, so the page is not |
| // eligible for BackForwardCache. |
| EXPECT_TRUE(main_rfh.WaitUntilRenderFrameDeleted()); |
| EXPECT_TRUE(sub_rfh.WaitUntilRenderFrameDeleted()); |
| EXPECT_FALSE(subframe_navigation_manager.was_committed()); |
| |
| // Navigate back. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| ExpectNotRestored({NotRestoredReason::kSubframeIsNavigating}, {}, {}, {}, {}, |
| FROM_HERE); |
| |
| // Confirm that subframe's url didn't change. |
| EXPECT_EQ(subframe_url, current_frame_host()->child_at(0)->current_url()); |
| } |
| |
| // Confirm that subframe navigation which needs url loader that hasn't sent a |
| // network request should not block BackForwardCache. |
| IN_PROC_BROWSER_TEST_F( |
| BackForwardCacheWithSubframeNavigationBrowserTest, |
| SubframeNavigationWithUrlLoaderBeforeSendingRequestShouldNotPreventCache) { |
| // This test relies on the main frame and the iframe to live in different |
| // processes. This allows one renderer process to proceed a navigation while |
| // the other renderer process is busy executing its beforeunload handler. |
| if (!AreAllSitesIsolatedForTesting()) { |
| GTEST_SKIP() << "Site isolation is not enabled!"; |
| } |
| const GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| const GURL subframe_url = embedded_test_server()->GetURL( |
| "b.com", "/cross_site_iframe_factory.html?b()"); |
| const GURL navigate_url( |
| embedded_test_server()->GetURL("c.com", "/title1.html")); |
| const GURL subframe_navigate_url( |
| embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // Navigate to a page with a cross site iframe. |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImplWrapper main_rfh(current_frame_host()); |
| RenderFrameHostImplWrapper sub_rfh( |
| main_rfh.get()->child_at(0)->current_frame_host()); |
| |
| // Put a page which has a subframe with a URLLoader navigation which hasn't |
| // sent a network request into BackForwardCache. The iframe itself |
| // does have a dialog-showing beforeunload handler. |
| TestNavigationManager subframe_navigation_manager(web_contents(), |
| subframe_navigate_url); |
| BFCachePageWithSubframeNavigationBeforeDidStartNavigation( |
| navigate_url, subframe_navigate_url, sub_rfh, subframe_navigation_manager, |
| /*iframe_id=*/"child-0"); |
| |
| // Navigate back. |
| TestNavigationObserver back_load_observer(shell()->web_contents()); |
| web_contents()->GetController().GoBack(); |
| back_load_observer.WaitForNavigationFinished(); |
| ASSERT_FALSE(main_rfh->IsInBackForwardCache()); |
| |
| // Wait until the resumed subframe navigation finishes. |
| EXPECT_TRUE(subframe_navigation_manager.WaitForNavigationFinished()); |
| EXPECT_TRUE(subframe_navigation_manager.was_successful()); |
| EXPECT_EQ(subframe_navigate_url, |
| current_frame_host()->child_at(0)->current_url()); |
| } |
| |
| // Confirm that subframe no-url loader navigation (e.g., about:blank) in |
| // bfcached page is deferred and then resumed when the page is navigated back. |
| IN_PROC_BROWSER_TEST_F( |
| BackForwardCacheWithSubframeNavigationBrowserTest, |
| SubframeNavigationWithoutUrlLoaderBeforeCommitShouldNotPreventCache) { |
| // This test relies on the main frame and the iframe to live in different |
| // processes. This allows one renderer process to proceed a navigation while |
| // the other renderer process is busy executing its beforeunload handler. |
| if (!AreAllSitesIsolatedForTesting()) { |
| GTEST_SKIP() << "Site isolation is not enabled!"; |
| } |
| const GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| const GURL navigate_url( |
| embedded_test_server()->GetURL("c.com", "/title1.html")); |
| const GURL subframe_navigate_url = GURL("about:blank"); |
| |
| // Navigate to a page with a cross site iframe. |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImplWrapper main_rfh(current_frame_host()); |
| RenderFrameHostImplWrapper sub_rfh( |
| main_rfh.get()->child_at(0)->current_frame_host()); |
| |
| // Put a page which has a subframe with a no-URLLoader navigation which hasn't |
| // reached the "pending commit" stage into BackForwardCache. The iframe itself |
| // does have a dialog-showing beforeunload handler. |
| TestNavigationManager subframe_navigation_manager(web_contents(), |
| subframe_navigate_url); |
| BFCachePageWithSubframeNavigationBeforeDidStartNavigation( |
| navigate_url, subframe_navigate_url, sub_rfh, subframe_navigation_manager, |
| /*iframe_id=*/"child-0"); |
| |
| // Navigate back. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| ASSERT_FALSE(main_rfh->IsInBackForwardCache()); |
| |
| // Confirm the deferred navigation was resumed and subframe's url changed. |
| EXPECT_TRUE(subframe_navigation_manager.WaitForNavigationFinished()); |
| EXPECT_TRUE(subframe_navigation_manager.was_successful()); |
| EXPECT_EQ(subframe_navigate_url, |
| current_frame_host()->child_at(0)->current_url()); |
| } |
| |
| // Confirm that we don't resume a subframe navigation when an unrelated BFCached |
| // page gets restored. |
| IN_PROC_BROWSER_TEST_P( |
| BackForwardCacheWithSubframeNavigationWithParamBrowserTest, |
| SubframeNavigationShouldNotBeResumedWhenUnrelatedPageRestored) { |
| // This test relies on the main frame and the iframe to live in different |
| // processes. This allows one renderer process to proceed a navigation while |
| // the other renderer process is busy executing its beforeunload handler. |
| if (!AreAllSitesIsolatedForTesting()) { |
| GTEST_SKIP() << "Site isolation is not enabled!"; |
| } |
| const GURL main_url_a(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| const GURL navigate_url_c( |
| embedded_test_server()->GetURL("c.com", "/title1.html")); |
| const GURL navigate_url_d( |
| embedded_test_server()->GetURL("d.com", "/title1.html")); |
| const GURL subframe_navigate_url = |
| GetParam() == SubframeNavigationType::WithURLLoader |
| ? embedded_test_server()->GetURL("b.com", "/title1.html") |
| : GURL("about:blank"); |
| |
| // Navigate to a page with a cross site iframe. |
| ASSERT_TRUE(NavigateToURL(shell(), main_url_a)); |
| RenderFrameHostImplWrapper main_rfh_a(current_frame_host()); |
| RenderFrameHostImplWrapper sub_rfh_b( |
| main_rfh_a.get()->child_at(0)->current_frame_host()); |
| |
| // Put a page which has a subframe with a navigation which hasn't reached the |
| // "pending commit" stage or sent a network request into BackForwardCache. |
| TestNavigationManager subframe_navigation_manager(web_contents(), |
| subframe_navigate_url); |
| BFCachePageWithSubframeNavigationBeforeDidStartNavigation( |
| navigate_url_c, subframe_navigate_url, sub_rfh_b, |
| subframe_navigation_manager, |
| /*iframe_id=*/"child-0"); |
| |
| // Navigate away. |
| // Currently, `main_rfh_a` is in BFCache and we are on `navigate_url_c`. Then, |
| // we will navigate to `navigate_url_d` which will put `main_rfh_c` in |
| // BFCache. |
| RenderFrameHostImplWrapper main_rfh_c(current_frame_host()); |
| ASSERT_TRUE(NavigateToURL(shell(), navigate_url_d)); |
| ASSERT_TRUE(main_rfh_c->IsInBackForwardCache()); |
| |
| // Navigate back to `main_rfh_c` and restore that from BFCache, while |
| // `main_rfh_a` is still in BFCache. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| ASSERT_EQ(main_rfh_c.get(), current_frame_host()); |
| ASSERT_TRUE(main_rfh_a->IsInBackForwardCache()); |
| |
| // Confirm the subframe's deferred navigation is not committed. |
| EXPECT_FALSE(subframe_navigation_manager.was_committed()); |
| |
| // Navigate back to `main_rfh_a`. |
| TestNavigationObserver back_load_observer(shell()->web_contents()); |
| web_contents()->GetController().GoBack(); |
| back_load_observer.WaitForNavigationFinished(); |
| ASSERT_FALSE(main_rfh_a->IsInBackForwardCache()); |
| |
| // Confirm the deferred navigation was resumed and subframe's url changed. |
| EXPECT_TRUE(subframe_navigation_manager.WaitForNavigationFinished()); |
| EXPECT_TRUE(subframe_navigation_manager.was_successful()); |
| EXPECT_EQ(subframe_navigate_url, |
| current_frame_host()->child_at(0)->current_url()); |
| } |
| |
| // Evict the bfcached page which has a subframe with a deferred navigation and |
| // confirm the subframe'url didn't change when the page is navigated back. |
| IN_PROC_BROWSER_TEST_P( |
| BackForwardCacheWithSubframeNavigationWithParamBrowserTest, |
| EvictBFCachedPageWithDeferredSubframeNavigationBeforeCommit) { |
| // This test relies on the main frame and the iframe to live in different |
| // processes. This allows one renderer process to proceed a navigation while |
| // the other renderer process is busy executing its beforeunload handler. |
| if (!AreAllSitesIsolatedForTesting()) { |
| GTEST_SKIP() << "Site isolation is not enabled!"; |
| } |
| const GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)")); |
| const GURL subframe_url = embedded_test_server()->GetURL( |
| "b.com", "/cross_site_iframe_factory.html?b()"); |
| const GURL navigate_url( |
| embedded_test_server()->GetURL("c.com", "/title1.html")); |
| const GURL subframe_navigate_url = |
| GetParam() == SubframeNavigationType::WithURLLoader |
| ? embedded_test_server()->GetURL("b.com", "/title1.html") |
| : GURL("about:blank"); |
| |
| // Navigate to a page with a cross site iframe. |
| ASSERT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImplWrapper main_rfh(current_frame_host()); |
| RenderFrameHostImplWrapper sub_rfh( |
| main_rfh.get()->child_at(0)->current_frame_host()); |
| |
| // Put a page which has a subframe with a navigation which hasn't reached the |
| // "pending commit" stage or sent a network request into BackForwardCache. The |
| // iframe itself does have a dialog-showing beforeunload handler. |
| TestNavigationManager subframe_navigation_manager(web_contents(), |
| subframe_navigate_url); |
| BFCachePageWithSubframeNavigationBeforeDidStartNavigation( |
| navigate_url, subframe_navigate_url, sub_rfh, subframe_navigation_manager, |
| /*iframe_id=*/"child-0"); |
| |
| // Flush the cache and evict the previously BFCached page. |
| web_contents()->GetController().GetBackForwardCache().Flush(); |
| ASSERT_TRUE(main_rfh.WaitUntilRenderFrameDeleted()); |
| ASSERT_TRUE(sub_rfh.WaitUntilRenderFrameDeleted()); |
| |
| // Confirm the subframe's deferred navigation has finished and was not |
| // committed. |
| EXPECT_TRUE(subframe_navigation_manager.WaitForNavigationFinished()); |
| EXPECT_FALSE(subframe_navigation_manager.was_committed()); |
| |
| // Navigate back. |
| ASSERT_TRUE(HistoryGoBack(web_contents())); |
| |
| // Confirm that subframe's url didn't change. |
| EXPECT_EQ(subframe_url, current_frame_host()->child_at(0)->current_url()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| BackForwardCacheWithSubframeNavigationWithParamBrowserTest, |
| ::testing::Values(SubframeNavigationType::WithoutURLLoader, |
| SubframeNavigationType::WithURLLoader), |
| &BackForwardCacheWithSubframeNavigationWithParamBrowserTest:: |
| DescribeParams); |
| |
| class BackForwardCacheFencedFrameBrowserTest |
| : public BackForwardCacheBrowserTest { |
| public: |
| BackForwardCacheFencedFrameBrowserTest() = default; |
| ~BackForwardCacheFencedFrameBrowserTest() override = default; |
| BackForwardCacheFencedFrameBrowserTest( |
| const BackForwardCacheFencedFrameBrowserTest&) = delete; |
| |
| BackForwardCacheFencedFrameBrowserTest& operator=( |
| const BackForwardCacheFencedFrameBrowserTest&) = delete; |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| BackForwardCacheBrowserTest::SetUpCommandLine(command_line); |
| fenced_frame_helper_ = std::make_unique<test::FencedFrameTestHelper>(); |
| } |
| |
| test::FencedFrameTestHelper& fenced_frame_test_helper() { |
| return *fenced_frame_helper_; |
| } |
| |
| private: |
| std::unique_ptr<test::FencedFrameTestHelper> fenced_frame_helper_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheFencedFrameBrowserTest, |
| FencedFramePageNotStoredInBackForwardCache) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b( |
| embedded_test_server()->GetURL("b.com", "/fenced_frames/title1.html")); |
| GURL url_c( |
| embedded_test_server()->GetURL("c.com", "/fenced_frames/title1.html")); |
| |
| // 1) Navigate to A. |
| EXPECT_TRUE(NavigateToURL(shell(), url_a)); |
| |
| // 2) Create a fenced frame. |
| content::RenderFrameHostImpl* fenced_frame_host = |
| static_cast<content::RenderFrameHostImpl*>( |
| fenced_frame_test_helper().CreateFencedFrame( |
| web_contents()->GetPrimaryMainFrame(), url_b)); |
| RenderFrameHostWrapper fenced_frame_host_wrapper(fenced_frame_host); |
| |
| // 3) Navigate to C on the fenced frame host. |
| fenced_frame_test_helper().NavigateFrameInFencedFrameTree(fenced_frame_host, |
| url_c); |
| EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); |
| |
| if (!fenced_frame_host_wrapper.IsRenderFrameDeleted()) |
| EXPECT_FALSE(fenced_frame_host->IsInBackForwardCache()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTest, |
| RendererInitiatedNavigateToSameUrl) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| |
| // 1) Navigate to A. |
| ASSERT_TRUE(NavigateToURL(shell(), url_a)); |
| RenderFrameHostImplWrapper rfh_a(current_frame_host()); |
| |
| // 2) Navigate to B. |
| ASSERT_TRUE(NavigateToURL(shell(), url_b)); |
| RenderFrameHostImplWrapper rfh_b(current_frame_host()); |
| |
| // 3) Navigate to B again, renderer initiated. |
| ASSERT_TRUE(NavigateToURLFromRenderer(rfh_b.get(), url_b)); |
| RenderFrameHostImplWrapper rfh_b2(current_frame_host()); |
| |
| // This is treated as replacement, and the previous B page did not get into |
| // back/forward cache. |
| if (ShouldCreateNewHostForAllFrames()) { |
| EXPECT_TRUE(rfh_b.WaitUntilRenderFrameDeleted()); |
| } else { |
| EXPECT_FALSE(rfh_b->IsInBackForwardCache()); |
| EXPECT_EQ(rfh_b.get(), rfh_b2.get()); |
| } |
| |
| // 4) Go back. Make sure we go back to A instead of B and restore from |
| // bfcache. |
| ASSERT_TRUE(HistoryGoBack(shell()->web_contents())); |
| EXPECT_EQ(current_frame_host(), rfh_a.get()); |
| EXPECT_TRUE(rfh_b2.get()->IsInBackForwardCache()); |
| ExpectRestored(FROM_HERE); |
| |
| // 5) Go forward and restore from bfcache. |
| ASSERT_TRUE(HistoryGoForward(shell()->web_contents())); |
| EXPECT_EQ(current_frame_host(), rfh_b2.get()); |
| ExpectRestored(FROM_HERE); |
| } |
| |
| // BEFORE ADDING A NEW TEST HERE |
| // Read the note at the top about the other files you could add it to. |
| } // namespace content |