| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/prerender/prerender_host.h" |
| |
| #include "base/test/scoped_feature_list.h" |
| #include "build/build_config.h" |
| #include "content/browser/prerender/prerender_attributes.h" |
| #include "content/browser/prerender/prerender_host_registry.h" |
| #include "content/browser/site_instance_impl.h" |
| #include "content/public/test/mock_web_contents_observer.h" |
| #include "content/public/test/navigation_simulator.h" |
| #include "content/public/test/test_browser_context.h" |
| #include "content/test/mock_commit_deferring_condition.h" |
| #include "content/test/navigation_simulator_impl.h" |
| #include "content/test/test_render_frame_host.h" |
| #include "content/test/test_render_view_host.h" |
| #include "content/test/test_web_contents.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/loader/loader_constants.h" |
| |
| namespace content { |
| namespace { |
| |
| using ::testing::_; |
| |
| // TODO(nhiroki): Merge this into TestNavigationObserver for code |
| // simplification. |
| class ActivationObserver : public PrerenderHost::Observer { |
| public: |
| // PrerenderHost::Observer implementations. |
| void OnActivated() override { was_activated_ = true; } |
| void OnHostDestroyed() override { |
| was_host_destroyed_ = true; |
| if (quit_closure_) { |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, std::move(quit_closure_)); |
| } |
| } |
| |
| void WaitUntilHostDestroyed() { |
| if (was_host_destroyed_) |
| return; |
| base::RunLoop run_loop; |
| quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| |
| bool was_activated() const { return was_activated_; } |
| |
| private: |
| base::OnceClosure quit_closure_; |
| bool was_activated_ = false; |
| bool was_host_destroyed_ = false; |
| }; |
| |
| // Finish a prerendering navigation that was already started with |
| // CreateAndStartHost(). |
| void CommitPrerenderNavigation(PrerenderHost& host) { |
| // Normally we could use EmbeddedTestServer to provide a response, but these |
| // tests use RenderViewHostImplTestHarness so the load goes through a |
| // TestNavigationURLLoader which we don't have access to in order to |
| // complete. Use NavigationSimulator to finish the navigation. |
| FrameTreeNode* ftn = FrameTreeNode::From(host.GetPrerenderedMainFrameHost()); |
| std::unique_ptr<NavigationSimulator> sim = |
| NavigationSimulatorImpl::CreateFromPendingInFrame(ftn); |
| sim->Commit(); |
| EXPECT_TRUE(host.is_ready_for_activation()); |
| } |
| |
| std::unique_ptr<NavigationSimulatorImpl> CreateActivation( |
| const GURL& prerendering_url, |
| WebContentsImpl& web_contents) { |
| std::unique_ptr<NavigationSimulatorImpl> navigation = |
| NavigationSimulatorImpl::CreateRendererInitiated( |
| prerendering_url, web_contents.GetMainFrame()); |
| navigation->SetReferrer(blink::mojom::Referrer::New( |
| web_contents.GetMainFrame()->GetLastCommittedURL(), |
| network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin)); |
| return navigation; |
| } |
| |
| void ActivatePrerenderedPage(const GURL& prerendering_url, |
| WebContentsImpl& web_contents) { |
| // Make sure the page for `prerendering_url` has been prerendered. |
| PrerenderHostRegistry* registry = web_contents.GetPrerenderHostRegistry(); |
| PrerenderHost* prerender_host = |
| registry->FindHostByUrlForTesting(prerendering_url); |
| EXPECT_TRUE(prerender_host); |
| int prerender_host_id = prerender_host->frame_tree_node_id(); |
| |
| ActivationObserver activation_observer; |
| prerender_host->AddObserver(&activation_observer); |
| |
| // Activate the prerendered page. |
| std::unique_ptr<NavigationSimulatorImpl> navigation = |
| CreateActivation(prerendering_url, web_contents); |
| navigation->Commit(); |
| activation_observer.WaitUntilHostDestroyed(); |
| |
| EXPECT_EQ(web_contents.GetMainFrame()->GetLastCommittedURL(), |
| prerendering_url); |
| |
| EXPECT_TRUE(activation_observer.was_activated()); |
| EXPECT_EQ(registry->FindReservedHostById(prerender_host_id), nullptr); |
| } |
| |
| class TestWebContentsDelegate : public WebContentsDelegate { |
| public: |
| TestWebContentsDelegate() = default; |
| ~TestWebContentsDelegate() override = default; |
| bool IsPrerender2Supported() override { return true; } |
| }; |
| |
| class PrerenderHostTest : public RenderViewHostImplTestHarness { |
| public: |
| PrerenderHostTest() { |
| scoped_feature_list_.InitWithFeatures( |
| {blink::features::kPrerender2}, |
| // Disable the memory requirement of Prerender2 so the test can run on |
| // any bot. |
| {blink::features::kPrerender2MemoryControls}); |
| } |
| |
| ~PrerenderHostTest() override = default; |
| |
| void SetUp() override { |
| RenderViewHostImplTestHarness::SetUp(); |
| browser_context_ = std::make_unique<TestBrowserContext>(); |
| } |
| |
| void TearDown() override { |
| browser_context_.reset(); |
| RenderViewHostImplTestHarness::TearDown(); |
| } |
| |
| void ExpectFinalStatus(PrerenderHost::FinalStatus status) { |
| histogram_tester_.ExpectUniqueSample( |
| "Prerender.Experimental.PrerenderHostFinalStatus", status, 1); |
| } |
| |
| std::unique_ptr<TestWebContents> CreateWebContents(const GURL& url) { |
| std::unique_ptr<TestWebContents> web_contents(TestWebContents::Create( |
| browser_context_.get(), |
| SiteInstanceImpl::Create(browser_context_.get()))); |
| web_contents_delegate_ = std::make_unique<TestWebContentsDelegate>(); |
| web_contents->SetDelegate(web_contents_delegate_.get()); |
| web_contents->NavigateAndCommit(url); |
| return web_contents; |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| |
| std::unique_ptr<TestBrowserContext> browser_context_; |
| std::unique_ptr<TestWebContentsDelegate> web_contents_delegate_; |
| base::HistogramTester histogram_tester_; |
| }; |
| |
| TEST_F(PrerenderHostTest, Activate) { |
| const GURL kOriginUrl("https://example.com/"); |
| std::unique_ptr<TestWebContents> web_contents = CreateWebContents(kOriginUrl); |
| PrerenderHostRegistry* registry = web_contents->GetPrerenderHostRegistry(); |
| |
| // Start prerendering a page. |
| const GURL kPrerenderingUrl("https://example.com/next"); |
| int prerender_frame_tree_node_id = |
| web_contents->AddPrerender(kPrerenderingUrl); |
| PrerenderHost* prerender_host = |
| registry->FindNonReservedHostById(prerender_frame_tree_node_id); |
| CommitPrerenderNavigation(*prerender_host); |
| |
| // Perform a navigation in the primary frame tree which activates the |
| // prerendered page. |
| ActivatePrerenderedPage(kPrerenderingUrl, *web_contents); |
| ExpectFinalStatus(PrerenderHost::FinalStatus::kActivated); |
| } |
| |
| TEST_F(PrerenderHostTest, DontActivate) { |
| std::unique_ptr<TestWebContents> web_contents = |
| CreateWebContents(GURL("https://example.com/")); |
| PrerenderHostRegistry* registry = web_contents->GetPrerenderHostRegistry(); |
| const GURL kPrerenderingUrl("https://example.com/next"); |
| |
| // Start the prerendering navigation, but don't activate it. |
| const int prerender_frame_tree_node_id = |
| web_contents->AddPrerender(kPrerenderingUrl); |
| registry->CancelHost(prerender_frame_tree_node_id, |
| PrerenderHost::FinalStatus::kDestroyed); |
| ExpectFinalStatus(PrerenderHost::FinalStatus::kDestroyed); |
| } |
| |
| // Tests that main frame navigations in a prerendered page cannot occur even if |
| // they start after the prerendered page has been reserved for activation. |
| TEST_F(PrerenderHostTest, MainFrameNavigationForReservedHost) { |
| const GURL kOriginUrl("https://example.com/"); |
| std::unique_ptr<TestWebContents> web_contents = CreateWebContents(kOriginUrl); |
| PrerenderHostRegistry* registry = web_contents->GetPrerenderHostRegistry(); |
| |
| // Start prerendering a page. |
| const GURL kPrerenderingUrl("https://example.com/next"); |
| RenderFrameHostImpl* prerender_rfh = |
| web_contents->AddPrerenderAndCommitNavigation(kPrerenderingUrl); |
| const int prerender_ftn_id = prerender_rfh->GetFrameTreeNodeId(); |
| PrerenderHost* prerender_host = |
| registry->FindNonReservedHostById(prerender_ftn_id); |
| FrameTreeNode* ftn = prerender_rfh->frame_tree_node(); |
| EXPECT_FALSE(ftn->HasNavigation()); |
| |
| ActivationObserver activation_observer; |
| prerender_host->AddObserver(&activation_observer); |
| |
| // Now navigate the primary page to the prerendered URL so that we activate |
| // the prerender. Use a CommitDeferringCondition to pause activation |
| // before it completes. |
| std::unique_ptr<NavigationSimulatorImpl> navigation; |
| MockCommitDeferringConditionWrapper condition(/*is_ready_to_commit=*/false); |
| { |
| MockCommitDeferringConditionInstaller installer(condition.PassToDelegate()); |
| |
| // Start trying to activate the prerendered page. |
| navigation = CreateActivation(kPrerenderingUrl, *web_contents); |
| navigation->Start(); |
| |
| // Wait for the condition to pause the activation. |
| condition.WaitUntilInvoked(); |
| } |
| |
| // The request should be deferred by the condition. |
| NavigationRequest* navigation_request = |
| static_cast<NavigationRequest*>(navigation->GetNavigationHandle()); |
| EXPECT_TRUE( |
| navigation_request->IsCommitDeferringConditionDeferredForTesting()); |
| |
| // The primary page should still be the original page. |
| EXPECT_EQ(web_contents->GetLastCommittedURL(), kOriginUrl); |
| |
| const GURL kBadUrl("https://example2.test/"); |
| TestNavigationManager tno(web_contents.get(), kBadUrl); |
| |
| // Start a cross-origin navigation in the prerendered page. It should be |
| // cancelled. |
| auto navigation_2 = |
| NavigationSimulatorImpl::CreateRendererInitiated(kBadUrl, prerender_rfh); |
| navigation_2->Start(); |
| EXPECT_EQ(NavigationThrottle::CANCEL, |
| navigation_2->GetLastThrottleCheckResult()); |
| tno.WaitForNavigationFinished(); |
| EXPECT_FALSE(tno.was_committed()); |
| |
| // The cross-origin navigation cancels the activation. |
| condition.CallResumeClosure(); |
| activation_observer.WaitUntilHostDestroyed(); |
| EXPECT_FALSE(activation_observer.was_activated()); |
| EXPECT_EQ(registry->FindHostByUrlForTesting(kPrerenderingUrl), nullptr); |
| ExpectFinalStatus(PrerenderHost::FinalStatus::kMainFrameNavigation); |
| |
| // The activation falls back to regular navigation. |
| navigation->Commit(); |
| EXPECT_EQ(web_contents->GetMainFrame()->GetLastCommittedURL(), |
| kPrerenderingUrl); |
| } |
| |
| // Tests that an activation can successfully commit after the prerendering page |
| // has updated its PageState. |
| TEST_F(PrerenderHostTest, ActivationAfterPageStateUpdate) { |
| std::unique_ptr<TestWebContents> web_contents = |
| CreateWebContents(GURL("https://example.com/")); |
| RenderFrameHostImpl* initiator_rfh = web_contents->GetMainFrame(); |
| PrerenderHostRegistry* registry = web_contents->GetPrerenderHostRegistry(); |
| |
| // Start prerendering a page. |
| const GURL kPrerenderingUrl("https://example.com/next"); |
| PrerenderAttributes attributes{ |
| kPrerenderingUrl, PrerenderTriggerType::kSpeculationRule, Referrer()}; |
| const int prerender_frame_tree_node_id = |
| registry->CreateAndStartHost(attributes, *initiator_rfh); |
| PrerenderHost* prerender_host = |
| registry->FindNonReservedHostById(prerender_frame_tree_node_id); |
| CommitPrerenderNavigation(*prerender_host); |
| |
| FrameTreeNode* prerender_root_ftn = |
| FrameTreeNode::GloballyFindByID(prerender_frame_tree_node_id); |
| RenderFrameHostImpl* prerender_rfh = prerender_root_ftn->current_frame_host(); |
| NavigationEntryImpl* prerender_nav_entry = |
| prerender_root_ftn->frame_tree()->controller().GetLastCommittedEntry(); |
| FrameNavigationEntry* prerender_root_fne = |
| prerender_nav_entry->GetFrameEntry(prerender_root_ftn); |
| |
| blink::PageState page_state = |
| blink::PageState::CreateForTestingWithSequenceNumbers( |
| GURL("about:blank"), prerender_root_fne->item_sequence_number(), |
| prerender_root_fne->document_sequence_number()); |
| |
| // Update PageState for prerender RFH, causing it to become different from |
| // the one stored in RFH's last commit params. |
| static_cast<mojom::FrameHost*>(prerender_rfh)->UpdateState(page_state); |
| |
| // Perform a navigation in the primary frame tree which activates the |
| // prerendered page. The main expectation is that this navigation commits |
| // successfully and doesn't hit any DCHECKs. |
| ActivatePrerenderedPage(kPrerenderingUrl, *web_contents); |
| ExpectFinalStatus(PrerenderHost::FinalStatus::kActivated); |
| |
| // Ensure that the the page_state was preserved. |
| EXPECT_EQ(web_contents->GetMainFrame(), prerender_rfh); |
| NavigationEntryImpl* activated_nav_entry = |
| web_contents->GetController().GetLastCommittedEntry(); |
| EXPECT_EQ( |
| page_state, |
| activated_nav_entry->GetFrameEntry(web_contents->GetFrameTree()->root()) |
| ->page_state()); |
| } |
| |
| // Test that WebContentsObserver::LoadProgressChanged is not invoked when the |
| // page gets loaded while prerendering but is invoked on prerender activation. |
| // Check that in case the load is incomplete with load progress |
| // `kPartialLoadProgress`, we would see |
| // LoadProgressChanged(kPartialLoadProgress) called on activation. |
| TEST_F(PrerenderHostTest, LoadProgressChangedInvokedOnActivation) { |
| const GURL kOriginUrl("https://example.com/"); |
| std::unique_ptr<TestWebContents> web_contents = CreateWebContents(kOriginUrl); |
| WebContentsImpl* web_contents_impl = |
| static_cast<WebContentsImpl*>(web_contents.get()); |
| |
| web_contents_impl->set_minimum_delay_between_loading_updates_for_testing( |
| base::Milliseconds(0)); |
| |
| // Initialize a MockWebContentsObserver and ensure that LoadProgressChanged is |
| // not invoked while prerendering. |
| testing::NiceMock<MockWebContentsObserver> observer(web_contents_impl); |
| testing::InSequence s; |
| EXPECT_CALL(observer, LoadProgressChanged(testing::_)).Times(0); |
| |
| // Start prerendering a page and commit prerender navigation. |
| const GURL kPrerenderingUrl("https://example.com/next"); |
| constexpr double kPartialLoadProgress = 0.7; |
| RenderFrameHostImpl* prerender_rfh = |
| web_contents->AddPrerenderAndCommitNavigation(kPrerenderingUrl); |
| FrameTreeNode* ftn = prerender_rfh->frame_tree_node(); |
| EXPECT_FALSE(ftn->HasNavigation()); |
| |
| // Verify and clear all expectations on the mock observer before setting new |
| // ones. |
| testing::Mock::VerifyAndClearExpectations(&observer); |
| |
| // Activate the prerendered page. This should result in invoking |
| // LoadProgressChanged for the following cases: |
| { |
| // 1) During DidStartLoading LoadProgressChanged is invoked with |
| // kInitialLoadProgress value. |
| EXPECT_CALL(observer, LoadProgressChanged(blink::kInitialLoadProgress)); |
| |
| // Verify that DidFinishNavigation is invoked before final load progress |
| // notification. |
| EXPECT_CALL(observer, DidFinishNavigation(testing::_)); |
| |
| // 2) After DidCommitNavigationInternal on activation with |
| // LoadProgressChanged is invoked with kPartialLoadProgress value. |
| EXPECT_CALL(observer, LoadProgressChanged(kPartialLoadProgress)); |
| |
| // 3) During DidStopLoading LoadProgressChanged is invoked with |
| // kFinalLoadProgress. |
| EXPECT_CALL(observer, LoadProgressChanged(blink::kFinalLoadProgress)); |
| } |
| |
| // Set load_progress value to kPartialLoadProgress in prerendering state, |
| // this should result in invoking LoadProgressChanged(kPartialLoadProgress) on |
| // activation. |
| prerender_rfh->GetPage().set_load_progress(kPartialLoadProgress); |
| |
| // Perform a navigation in the primary frame tree which activates the |
| // prerendered page. |
| ActivatePrerenderedPage(kPrerenderingUrl, *web_contents); |
| ExpectFinalStatus(PrerenderHost::FinalStatus::kActivated); |
| } |
| |
| TEST_F(PrerenderHostTest, CancelPrerenderWhenTriggerGetsHidden) { |
| std::unique_ptr<TestWebContents> web_contents = |
| CreateWebContents(GURL("https://example.com/")); |
| const GURL kPrerenderingUrl = GURL("https://example.com/empty.html"); |
| RenderFrameHostImpl* initiator_rfh = web_contents->GetMainFrame(); |
| PrerenderHostRegistry* registry = web_contents->GetPrerenderHostRegistry(); |
| PrerenderAttributes attributes{ |
| kPrerenderingUrl, PrerenderTriggerType::kSpeculationRule, Referrer()}; |
| const int prerender_frame_tree_node_id = |
| registry->CreateAndStartHost(attributes, *initiator_rfh); |
| PrerenderHost* prerender_host = |
| registry->FindNonReservedHostById(prerender_frame_tree_node_id); |
| ASSERT_NE(prerender_host, nullptr); |
| CommitPrerenderNavigation(*prerender_host); |
| |
| // Changing the visibility state to HIDDEN will cause prerendering cancelled. |
| web_contents->WasHidden(); |
| ExpectFinalStatus(PrerenderHost::FinalStatus::kTriggerBackgrounded); |
| } |
| |
| TEST_F(PrerenderHostTest, DontCancelPrerenderWhenTriggerGetsVisible) { |
| std::unique_ptr<TestWebContents> web_contents = |
| CreateWebContents(GURL("https://example.com/")); |
| const GURL kPrerenderingUrl = GURL("https://example.com/empty.html"); |
| RenderFrameHostImpl* initiator_rfh = web_contents->GetMainFrame(); |
| PrerenderHostRegistry* registry = web_contents->GetPrerenderHostRegistry(); |
| PrerenderAttributes attributes{ |
| kPrerenderingUrl, PrerenderTriggerType::kSpeculationRule, Referrer()}; |
| const int prerender_frame_tree_node_id = |
| registry->CreateAndStartHost(attributes, *initiator_rfh); |
| PrerenderHost* prerender_host = |
| registry->FindNonReservedHostById(prerender_frame_tree_node_id); |
| ASSERT_NE(prerender_host, nullptr); |
| CommitPrerenderNavigation(*prerender_host); |
| |
| // Changing the visibility state to VISIBLE will not affect prerendering. |
| web_contents->WasShown(); |
| ActivatePrerenderedPage(kPrerenderingUrl, *web_contents); |
| ExpectFinalStatus(PrerenderHost::FinalStatus::kActivated); |
| } |
| |
| // Skip this test on Android as it doesn't support the OCCLUDED state. |
| #if !defined(OS_ANDROID) |
| TEST_F(PrerenderHostTest, DontCancelPrerenderWhenTriggerGetsOcculded) { |
| std::unique_ptr<TestWebContents> web_contents = |
| CreateWebContents(GURL("https://example.com/")); |
| const GURL kPrerenderingUrl = GURL("https://example.com/empty.html"); |
| RenderFrameHostImpl* initiator_rfh = web_contents->GetMainFrame(); |
| PrerenderHostRegistry* registry = web_contents->GetPrerenderHostRegistry(); |
| PrerenderAttributes attributes{ |
| kPrerenderingUrl, PrerenderTriggerType::kSpeculationRule, Referrer()}; |
| const int prerender_frame_tree_node_id = |
| registry->CreateAndStartHost(attributes, *initiator_rfh); |
| PrerenderHost* prerender_host = |
| registry->FindNonReservedHostById(prerender_frame_tree_node_id); |
| ASSERT_NE(prerender_host, nullptr); |
| CommitPrerenderNavigation(*prerender_host); |
| |
| // Changing the visibility state to OCCLUDED will not affect prerendering. |
| web_contents->WasOccluded(); |
| ActivatePrerenderedPage(kPrerenderingUrl, *web_contents); |
| ExpectFinalStatus(PrerenderHost::FinalStatus::kActivated); |
| } |
| #endif |
| |
| } // namespace |
| } // namespace content |