| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/barrier_closure.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/path_service.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "chrome/browser/chrome_content_browser_client.h" |
| #include "chrome/browser/preloading/preview/preview_manager.h" |
| #include "chrome/browser/preloading/preview/preview_test_util.h" |
| #include "chrome/browser/preloading/preview/preview_zoom_controller.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/test/base/chrome_test_utils.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/mojo_capability_control_test_util.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "mojo/public/cpp/bindings/remote_set.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/navigation/preloading_headers.h" |
| |
| class PreviewBrowserTest : public InProcessBrowserTest { |
| public: |
| PreviewBrowserTest() |
| : helper_(std::make_unique<test::PreviewTestHelper>( |
| base::BindRepeating(&PreviewBrowserTest::web_contents, |
| base::Unretained(this)))) {} |
| |
| void SetUp() override { InProcessBrowserTest::SetUp(); } |
| |
| void SetUpOnMainThread() override { |
| https_server_ = std::make_unique<net::EmbeddedTestServer>( |
| net::EmbeddedTestServer::TYPE_HTTPS); |
| |
| embedded_test_server()->RegisterRequestMonitor(base::BindRepeating( |
| &PreviewBrowserTest::MonitorResourceRequest, base::Unretained(this))); |
| https_server_->RegisterRequestMonitor(base::BindRepeating( |
| &PreviewBrowserTest::MonitorResourceRequest, base::Unretained(this))); |
| histogram_tester_ = std::make_unique<base::HistogramTester>(); |
| |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| |
| embedded_test_server()->AddDefaultHandlers(GetChromeTestDataDir()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| https_server_->SetSSLConfig( |
| net::test_server::EmbeddedTestServer::CERT_TEST_NAMES); |
| https_server_->AddDefaultHandlers(GetChromeTestDataDir()); |
| ASSERT_TRUE(https_server_->Start()); |
| } |
| |
| void TearDownOnMainThread() override { |
| ASSERT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete()); |
| ASSERT_TRUE(https_server_->ShutdownAndWaitUntilComplete()); |
| } |
| |
| GURL GetURL(const std::string& path) { |
| return https_server_->GetURL("a.test", path); |
| } |
| |
| net::test_server::HttpRequest::HeaderMap GetObservedRequestHeadersFor( |
| const GURL& url) { |
| base::AutoLock auto_lock(lock_); |
| std::string path = url.PathForRequest(); |
| return request_headers_by_path_[path]; |
| } |
| |
| net::EmbeddedTestServer& https_server() { return *https_server_.get(); } |
| |
| content::WebContents* web_contents() { |
| return chrome_test_utils::GetActiveWebContents(this); |
| } |
| |
| test::PreviewTestHelper& helper() { return *helper_.get(); } |
| base::HistogramTester& histogram_tester() { return *histogram_tester_.get(); } |
| |
| private: |
| void MonitorResourceRequest(const net::test_server::HttpRequest& request) { |
| ASSERT_FALSE( |
| content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| base::AutoLock auto_lock(lock_); |
| request_headers_by_path_.emplace(request.GetURL().PathForRequest(), |
| request.headers); |
| } |
| |
| std::unique_ptr<net::EmbeddedTestServer> https_server_; |
| |
| base::Lock lock_; |
| std::map<std::string, net::test_server::HttpRequest::HeaderMap> |
| request_headers_by_path_ GUARDED_BY(lock_); |
| |
| std::unique_ptr<test::PreviewTestHelper> helper_; |
| std::unique_ptr<base::HistogramTester> histogram_tester_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, SecPurposeHeader) { |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetURL("/title1.html"))); |
| |
| GURL preview_url = GetURL("/title2.html"); |
| helper().InitiatePreview(preview_url); |
| helper().WaitUntilLoadFinished(); |
| |
| net::test_server::HttpRequest::HeaderMap headers = |
| GetObservedRequestHeadersFor(preview_url); |
| auto it = headers.find(blink::kSecPurposeHeaderName); |
| ASSERT_NE(it, headers.end()); |
| EXPECT_EQ(it->second, blink::kSecPurposePrefetchPrerenderPreviewHeaderValue); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, CancelWhenPrimaryPageChanged) { |
| TabStripModel* tab_strip_model = browser()->tab_strip_model(); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetURL("/title1.html"))); |
| |
| helper().InitiatePreview(GetURL("/title2.html")); |
| helper().WaitUntilLoadFinished(); |
| |
| base::WeakPtr<content::WebContents> preview_web_contents = |
| helper().GetWebContentsForPreviewTab(); |
| ASSERT_TRUE(preview_web_contents); |
| |
| EXPECT_EQ(1, tab_strip_model->count()); |
| EXPECT_EQ(true, |
| content::EvalJs(preview_web_contents, "document.prerendering")); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetURL("/title1.html"))); |
| |
| ASSERT_FALSE(preview_web_contents); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, PromoteToNewTab) { |
| TabStripModel* tab_strip_model = browser()->tab_strip_model(); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetURL("/title1.html"))); |
| |
| helper().InitiatePreview(GetURL("/title2.html")); |
| helper().WaitUntilLoadFinished(); |
| |
| base::WeakPtr<content::WebContents> preview_web_contents = |
| helper().GetWebContentsForPreviewTab(); |
| ASSERT_TRUE(preview_web_contents); |
| |
| EXPECT_EQ(1, tab_strip_model->count()); |
| EXPECT_EQ(true, |
| content::EvalJs(preview_web_contents, "document.prerendering")); |
| |
| helper().PromoteToNewTab(); |
| |
| EXPECT_EQ(2, tab_strip_model->count()); |
| EXPECT_EQ(false, |
| content::EvalJs(preview_web_contents, "document.prerendering")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, TrivialSessionHistory) { |
| const std::string title1_path = "/title1.html"; |
| const GURL title1_url = GetURL(title1_path); |
| const GURL title2_url = GetURL("/title2.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), title1_url)); |
| |
| helper().InitiatePreview(title2_url); |
| helper().WaitUntilLoadFinished(); |
| |
| base::WeakPtr<content::WebContents> preview_web_contents = |
| helper().GetWebContentsForPreviewTab(); |
| ASSERT_TRUE(preview_web_contents); |
| |
| EXPECT_EQ(title2_url, preview_web_contents->GetLastCommittedURL()); |
| EXPECT_EQ(1, preview_web_contents->GetController().GetEntryCount()); |
| EXPECT_EQ( |
| 1, EvalJs(preview_web_contents->GetPrimaryMainFrame(), "history.length")); |
| |
| ASSERT_EQ(title1_path, EvalJs(preview_web_contents->GetPrimaryMainFrame(), |
| "location = '/title1.html';")); |
| helper().WaitUntilLoadFinished(); |
| |
| EXPECT_EQ(title1_url, preview_web_contents->GetLastCommittedURL()); |
| EXPECT_EQ(1, preview_web_contents->GetController().GetEntryCount()); |
| EXPECT_EQ( |
| 1, EvalJs(preview_web_contents->GetPrimaryMainFrame(), "history.length")); |
| } |
| |
| class MojoCapabilityControlTestContentBrowserClient |
| : public ChromeContentBrowserClient, |
| public content::test::MojoCapabilityControlTestHelper { |
| public: |
| MojoCapabilityControlTestContentBrowserClient() { |
| previous_client_ = content::SetBrowserClientForTesting(this); |
| } |
| ~MojoCapabilityControlTestContentBrowserClient() override { |
| content::SetBrowserClientForTesting(previous_client_); |
| } |
| MojoCapabilityControlTestContentBrowserClient( |
| const MojoCapabilityControlTestContentBrowserClient&) = delete; |
| MojoCapabilityControlTestContentBrowserClient& operator=( |
| const MojoCapabilityControlTestContentBrowserClient&) = delete; |
| |
| private: |
| // ChromeContentBrowserClient implementation. |
| void RegisterBrowserInterfaceBindersForFrame( |
| content::RenderFrameHost* render_frame_host, |
| mojo::BinderMapWithContext<content::RenderFrameHost*>* map) override { |
| if (previous_client_) { |
| previous_client_->RegisterBrowserInterfaceBindersForFrame( |
| render_frame_host, map); |
| } |
| RegisterTestBrowserInterfaceBindersForFrame(render_frame_host, map); |
| } |
| void RegisterMojoBinderPoliciesForPreview( |
| content::MojoBinderPolicyMap& policy_map) override { |
| RegisterTestMojoBinderPolicies(policy_map); |
| } |
| |
| raw_ptr<content::ContentBrowserClient> previous_client_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, MojoCapabilityControl) { |
| MojoCapabilityControlTestContentBrowserClient test_browser_client; |
| |
| const GURL kInitialUrl = GetURL("/title1.html"); |
| const GURL kPreviewUrl = GetURL("/page_with_iframe.html"); |
| |
| // Navigate to an initial page. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), kInitialUrl)); |
| ASSERT_EQ(web_contents()->GetLastCommittedURL(), kInitialUrl); |
| |
| // Open the target page in preview mode. |
| helper().InitiatePreview(kPreviewUrl); |
| helper().WaitUntilLoadFinished(); |
| |
| // Gather RenderFrameHosts for the preview tab. |
| std::vector<content::RenderFrameHost*> frames; |
| base::WeakPtr<content::WebContents> preview_web_contents = |
| helper().GetWebContentsForPreviewTab(); |
| ASSERT_TRUE(preview_web_contents); |
| preview_web_contents->GetPrimaryMainFrame()->ForEachRenderFrameHost( |
| [&](content::RenderFrameHost* rfh) { frames.push_back(rfh); }); |
| CHECK_EQ(frames.size(), 2U); |
| |
| // A barrier closure to wait until a deferred interface is granted on all |
| // frames. |
| base::RunLoop run_loop; |
| auto barrier_closure = |
| base::BarrierClosure(frames.size(), run_loop.QuitClosure()); |
| |
| mojo::RemoteSet<content::mojom::TestInterfaceForDefer> defer_remote_set; |
| mojo::RemoteSet<content::mojom::TestInterfaceForGrant> grant_remote_set; |
| for (auto* frame : frames) { |
| // Try to bind a kDefer interface. |
| mojo::Remote<content::mojom::TestInterfaceForDefer> defer_remote; |
| test_browser_client.GetInterface(frame, |
| defer_remote.BindNewPipeAndPassReceiver()); |
| // The barrier closure will be called after the deferred interface is |
| // granted. |
| defer_remote->Ping(barrier_closure); |
| defer_remote_set.Add(std::move(defer_remote)); |
| |
| // Try to bind a kGrant interface. |
| mojo::Remote<content::mojom::TestInterfaceForGrant> grant_remote; |
| test_browser_client.GetInterface(frame, |
| grant_remote.BindNewPipeAndPassReceiver()); |
| grant_remote_set.Add(std::move(grant_remote)); |
| } |
| |
| // Verify that BrowserInterfaceBrokerImpl defers running binders whose |
| // policies are kDefer until the prerendered page is activated. |
| EXPECT_EQ(test_browser_client.GetDeferReceiverSetSize(), 0U); |
| // Verify that BrowserInterfaceBrokerImpl executes kGrant binders immediately. |
| EXPECT_EQ(test_browser_client.GetGrantReceiverSetSize(), frames.size()); |
| |
| // Activate the prerendered page. |
| helper().PromoteToNewTab(); |
| |
| // Wait until the deferred interface is granted on all frames. |
| run_loop.Run(); |
| EXPECT_EQ(test_browser_client.GetDeferReceiverSetSize(), frames.size()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, ErrorOnNonHttpsUrl) { |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetURL("/title1.html"))); |
| |
| helper().InitiatePreview(embedded_test_server()->GetURL("/title2.html")); |
| helper().WaitUntilLoadFinished(); |
| |
| base::WeakPtr<content::WebContents> preview_web_contents = |
| helper().GetWebContentsForPreviewTab(); |
| ASSERT_TRUE(preview_web_contents); |
| |
| EXPECT_EQ("Can't preview non-HTTPS URL", |
| content::EvalJs(preview_web_contents, |
| "document.querySelector('h1 span').textContent") |
| .ExtractString()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, ErrorOnRedirectionToNonHttpsUrl) { |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetURL("/title1.html"))); |
| |
| GURL http_url = embedded_test_server()->GetURL("/title2.html"); |
| GURL preview_url = GetURL("/server-redirect?" + http_url.spec()); |
| helper().InitiatePreview(preview_url); |
| helper().WaitUntilLoadFinished(); |
| |
| base::WeakPtr<content::WebContents> preview_web_contents = |
| helper().GetWebContentsForPreviewTab(); |
| ASSERT_TRUE(preview_web_contents); |
| |
| EXPECT_EQ("Can't preview non-HTTPS URL", |
| content::EvalJs(preview_web_contents, |
| "document.querySelector('h1 span').textContent") |
| .ExtractString()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, ZoomUsageRecordedOnCancel) { |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetURL("/title1.html"))); |
| |
| helper().InitiatePreview(GetURL("/title2.html")); |
| helper().WaitUntilLoadFinished(); |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetURL("/title1.html"))); |
| |
| histogram_tester().ExpectTotalCount("LinkPreview.Experimental.ZoomUsage", 1); |
| histogram_tester().ExpectBucketCount( |
| "LinkPreview.Experimental.ZoomUsage", |
| PreviewZoomController::ZoomUsage::kOnlyDefaultUsed, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, ZoomUsageRecordedOnPromoteToNewTab) { |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetURL("/title1.html"))); |
| |
| helper().InitiatePreview(GetURL("/title2.html")); |
| helper().WaitUntilLoadFinished(); |
| |
| helper().PromoteToNewTab(); |
| // Wait a task destructing PreviewTab. |
| base::RunLoop().RunUntilIdle(); |
| |
| histogram_tester().ExpectTotalCount("LinkPreview.Experimental.ZoomUsage", 1); |
| histogram_tester().ExpectBucketCount( |
| "LinkPreview.Experimental.ZoomUsage", |
| PreviewZoomController::ZoomUsage::kOnlyDefaultUsed, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(PreviewBrowserTest, ZoomIsDurableForHost) { |
| GURL url_a = https_server().GetURL("a.test", "/title1.html"); |
| GURL url_b = https_server().GetURL("b.test", "/title1.html"); |
| |
| auto expect = [this](int x, int y) { |
| histogram_tester().ExpectTotalCount("LinkPreview.Experimental.ZoomUsage", |
| x + y); |
| histogram_tester().ExpectBucketCount( |
| "LinkPreview.Experimental.ZoomUsage", |
| PreviewZoomController::ZoomUsage::kOnlyDefaultUsed, x); |
| histogram_tester().ExpectBucketCount( |
| "LinkPreview.Experimental.ZoomUsage", |
| PreviewZoomController::ZoomUsage::kNonDefaultUsed, y); |
| }; |
| |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_a)); |
| helper().InitiatePreview(url_a); |
| helper().WaitUntilLoadFinished(); |
| |
| // This preview contributes to kNonDefaultUsed. |
| helper().GetManager().PreviewZoomControllerForTesting()->Zoom( |
| content::PAGE_ZOOM_IN); |
| |
| // Navigate out and cancel preview. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_b)); |
| |
| expect(0, 1); |
| |
| // This preview will be shown with the default zoom level because host |
| // differs. |
| helper().InitiatePreview(url_b); |
| helper().WaitUntilLoadFinished(); |
| |
| // Navigate out and cancel preview. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_a)); |
| |
| expect(1, 1); |
| |
| // This preview will be shown with zoom-in due to the effect of first preview. |
| helper().InitiatePreview(url_a); |
| helper().WaitUntilLoadFinished(); |
| |
| // This preview contributes to kNonDefaultUsed. The next doesn't. |
| helper().GetManager().PreviewZoomControllerForTesting()->Zoom( |
| content::PAGE_ZOOM_RESET); |
| |
| // Navigate out and cancel preview. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_a)); |
| |
| expect(1, 2); |
| |
| helper().InitiatePreview(url_a); |
| helper().WaitUntilLoadFinished(); |
| |
| // Navigate out and cancel preview. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url_a)); |
| |
| expect(2, 2); |
| } |