// 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 "base/test/scoped_feature_list.h" #include "base/test/test_timeouts.h" #include "content/browser/renderer_host/render_widget_host_impl.h" #include "content/public/browser/render_view_host.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/test/browser_test.h" #include "content/public/test/browser_test_utils.h" #include "content/public/test/content_browser_test.h" #include "content/public/test/content_browser_test_utils.h" #include "content/public/test/hit_test_region_observer.h" #include "content/public/test/test_navigation_observer.h" #include "content/shell/browser/shell.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 "url/gurl.h" namespace content { class TextFragmentAnchorBrowserTest : public ContentBrowserTest { public: TextFragmentAnchorBrowserTest() { feature_list_.InitAndEnableFeature(features::kDocumentPolicy); } protected: void SetUpOnMainThread() override { host_resolver()->AddRule("*", "127.0.0.1"); } void SetUpCommandLine(base::CommandLine* command_line) override { ContentBrowserTest::SetUpCommandLine(command_line); command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures, "TextFragmentIdentifiers"); } // Simulates a click on the middle of the DOM element with the given |id|. void ClickElementWithId(WebContents* web_contents, const std::string& id) { // Get the center coordinates of the DOM element. const int x = EvalJs(web_contents, JsReplace("const bounds = " "document.getElementById($1)." "getBoundingClientRect();" "Math.floor(bounds.left + bounds.width / 2)", id)) .ExtractInt(); const int y = EvalJs(web_contents, JsReplace("const bounds = " "document.getElementById($1)." "getBoundingClientRect();" "Math.floor(bounds.top + bounds.height / 2)", id)) .ExtractInt(); SimulateMouseClickAt(web_contents, 0, blink::WebMouseEvent::Button::kLeft, gfx::Point(x, y)); } void WaitForPageLoad(WebContents* contents) { EXPECT_TRUE(WaitForLoadStop(contents)); EXPECT_TRUE(WaitForRenderFrameReady(contents->GetMainFrame())); } RenderWidgetHostImpl* GetWidgetHost() { return RenderWidgetHostImpl::From( shell()->web_contents()->GetRenderViewHost()->GetWidget()); } base::test::ScopedFeatureList feature_list_; }; IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, EnabledOnUserNavigation) { ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL("/target_text_link.html")); GURL target_text_url(embedded_test_server()->GetURL( "/scrollable_page_with_content.html#:~:text=text")); EXPECT_TRUE(NavigateToURL(shell(), url)); WebContents* main_contents = shell()->web_contents(); TestNavigationObserver observer(main_contents); RenderFrameSubmissionObserver frame_observer(main_contents); // We need to wait until hit test data is available. HitTestRegionObserver hittest_observer(GetWidgetHost()->GetFrameSinkId()); hittest_observer.WaitForHitTestData(); ClickElementWithId(main_contents, "link"); observer.Wait(); EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL()); WaitForPageLoad(main_contents); frame_observer.WaitForScrollOffsetAtTop( /*expected_scroll_offset_at_top=*/false); RunUntilInputProcessed(GetWidgetHost()); EXPECT_EQ(true, EvalJs(main_contents, "did_scroll;")); } IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, EnabledOnBrowserNavigation) { ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL( "/scrollable_page_with_content.html#:~:text=text")); WebContents* main_contents = shell()->web_contents(); RenderFrameSubmissionObserver frame_observer(main_contents); EXPECT_TRUE(NavigateToURL(shell(), url)); WaitForPageLoad(main_contents); frame_observer.WaitForScrollOffsetAtTop( /*expected_scroll_offset_at_top=*/false); RunUntilInputProcessed(GetWidgetHost()); EXPECT_EQ(true, EvalJs(main_contents, "did_scroll;")); } IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, EnabledOnUserGestureScriptNavigation) { ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL("/empty.html")); GURL target_text_url(embedded_test_server()->GetURL( "/scrollable_page_with_content.html#:~:text=text")); EXPECT_TRUE(NavigateToURL(shell(), url)); WebContents* main_contents = shell()->web_contents(); TestNavigationObserver observer(main_contents); RenderFrameSubmissionObserver frame_observer(main_contents); // ExecuteScript executes with a user gesture EXPECT_TRUE(ExecuteScript(main_contents, "location = '" + target_text_url.spec() + "';")); observer.Wait(); EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL()); WaitForPageLoad(main_contents); frame_observer.WaitForScrollOffsetAtTop( /*expected_scroll_offset_at_top=*/false); RunUntilInputProcessed(GetWidgetHost()); EXPECT_EQ(true, EvalJs(main_contents, "did_scroll;")); } IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, DisabledOnScriptNavigation) { ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL("/empty.html")); GURL target_text_url(embedded_test_server()->GetURL( "/scrollable_page_with_content.html#:~:text=text")); EXPECT_TRUE(NavigateToURL(shell(), url)); WebContents* main_contents = shell()->web_contents(); TestNavigationObserver observer(main_contents); EXPECT_TRUE(ExecuteScriptWithoutUserGesture( main_contents, "location = '" + target_text_url.spec() + "';")); observer.Wait(); EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL()); WaitForPageLoad(main_contents); // Wait a short amount of time to ensure the page does not scroll. base::RunLoop run_loop; base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); run_loop.Run(); RunUntilInputProcessed(GetWidgetHost()); EXPECT_EQ(false, EvalJs(main_contents, "did_scroll;")); } IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, DisabledOnScriptHistoryNavigation) { ASSERT_TRUE(embedded_test_server()->Start()); GURL target_text_url(embedded_test_server()->GetURL( "/scrollable_page_with_content.html#:~:text=text")); GURL url(embedded_test_server()->GetURL("/empty.html")); EXPECT_TRUE(NavigateToURL(shell(), target_text_url)); WebContents* main_contents = shell()->web_contents(); RenderFrameSubmissionObserver frame_observer(main_contents); frame_observer.WaitForScrollOffsetAtTop(false); // Scroll the page back to top so scroll restoration does not scroll the // target back into view. EXPECT_TRUE(ExecuteScript(main_contents, "window.scrollTo(0, 0)")); frame_observer.WaitForScrollOffsetAtTop(true); EXPECT_TRUE(NavigateToURL(shell(), url)); TestNavigationObserver observer(main_contents); EXPECT_TRUE(ExecuteScriptWithoutUserGesture(main_contents, "history.back()")); observer.Wait(); EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL()); WaitForPageLoad(main_contents); // Wait a short amount of time to ensure the page does not scroll. base::RunLoop run_loop; base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); run_loop.Run(); RunUntilInputProcessed(GetWidgetHost()); // Note: we use a scroll handler in the page to check whether any scrolls // happened at all, rather than checking the current scroll offset. This is // to ensure that if the offset is reset back to the top for other reasons // (e.g. history restoration) we still fail this test. See // https://crbug.com/1042986 for why this matters. EXPECT_EQ(false, EvalJs(main_contents, "did_scroll;")); } IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, EnabledOnSameDocumentBrowserNavigation) { ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL( "/scrollable_page_with_content.html#:~:text=text")); WebContents* main_contents = shell()->web_contents(); RenderFrameSubmissionObserver frame_observer(main_contents); EXPECT_TRUE(NavigateToURL(shell(), url)); WaitForPageLoad(main_contents); frame_observer.WaitForScrollOffsetAtTop(false); // Scroll the page back to top. Make sure we reset the |did_scroll| variable // we'll use below to ensure the same-document navigation invokes the text // fragment. EXPECT_TRUE(ExecuteScript(main_contents, "window.scrollTo(0, 0)")); frame_observer.WaitForScrollOffsetAtTop(true); EXPECT_TRUE(ExecJs(main_contents, "did_scroll = false;")); // Perform a same-document browser initiated navigation GURL same_doc_url(embedded_test_server()->GetURL( "/scrollable_page_with_content.html#:~:text=some")); EXPECT_TRUE(NavigateToURL(shell(), same_doc_url)); WaitForPageLoad(main_contents); frame_observer.WaitForScrollOffsetAtTop( /*expected_scroll_offset_at_top=*/false); RunUntilInputProcessed(GetWidgetHost()); EXPECT_EQ(true, EvalJs(main_contents, "did_scroll;")); } IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, DisabledOnSameDocumentScriptNavigation) { ASSERT_TRUE(embedded_test_server()->Start()); GURL url( embedded_test_server()->GetURL("/scrollable_page_with_content.html")); GURL target_text_url(embedded_test_server()->GetURL( "/scrollable_page_with_content.html#:~:text=some")); EXPECT_TRUE(NavigateToURL(shell(), url)); WebContents* main_contents = shell()->web_contents(); TestNavigationObserver observer(main_contents); EXPECT_TRUE(ExecuteScriptWithoutUserGesture( main_contents, "location = '" + target_text_url.spec() + "';")); observer.Wait(); EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL()); WaitForPageLoad(main_contents); // Wait a short amount of time to ensure the page does not scroll. base::RunLoop run_loop; base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); run_loop.Run(); RunUntilInputProcessed(GetWidgetHost()); EXPECT_EQ(false, EvalJs(main_contents, "did_scroll;")); } IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, EnabledByDocumentPolicy) { net::test_server::ControllableHttpResponse response(embedded_test_server(), "/target.html"); ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL("/target.html#:~:text=text")); WebContents* main_contents = shell()->web_contents(); RenderFrameSubmissionObserver frame_observer(main_contents); // Load the target document TestNavigationManager navigation_manager(main_contents, url); shell()->LoadURL(url); // Start navigation EXPECT_TRUE(navigation_manager.WaitForRequestStart()); navigation_manager.ResumeNavigation(); // Send Document-Policy header response.WaitForRequest(); response.Send( "HTTP/1.1 200 OK\r\n" "Content-Type: text/html; charset=utf-8\r\n" "Document-Policy: no-force-load-at-top\r\n" "\r\n" "" "
Some text
"); response.Done(); EXPECT_TRUE(navigation_manager.WaitForResponse()); navigation_manager.ResumeNavigation(); navigation_manager.WaitForNavigationFinished(); WaitForPageLoad(main_contents); frame_observer.WaitForScrollOffsetAtTop( /*expected_scroll_offset_at_top=*/false); RunUntilInputProcessed(GetWidgetHost()); EXPECT_EQ(true, EvalJs(main_contents, "did_scroll;")); } IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, DisabledByDocumentPolicy) { net::test_server::ControllableHttpResponse response(embedded_test_server(), "/target.html"); ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL("/target.html#:~:text=text")); WebContents* main_contents = shell()->web_contents(); // Load the target document TestNavigationManager navigation_manager(main_contents, url); shell()->LoadURL(url); // Start navigation EXPECT_TRUE(navigation_manager.WaitForRequestStart()); navigation_manager.ResumeNavigation(); // Send Document-Policy header response.WaitForRequest(); response.Send( "HTTP/1.1 200 OK\r\n" "Content-Type: text/html; charset=utf-8\r\n" "Document-Policy: force-load-at-top\r\n" "\r\n" "" "Some text
"); response.Done(); EXPECT_TRUE(navigation_manager.WaitForResponse()); navigation_manager.ResumeNavigation(); navigation_manager.WaitForNavigationFinished(); WaitForPageLoad(main_contents); // Wait a short amount of time to ensure the page does not scroll. base::RunLoop run_loop; base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); run_loop.Run(); RunUntilInputProcessed(GetWidgetHost()); EXPECT_EQ(false, EvalJs(main_contents, "did_scroll;")); } class ForceLoadAtTopBrowserTest : public TextFragmentAnchorBrowserTest { protected: void SetUpOnMainThread() override { TextFragmentAnchorBrowserTest::SetUpOnMainThread(); ASSERT_TRUE(embedded_test_server()->Start()); } void SetUpCommandLine(base::CommandLine* command_line) override { TextFragmentAnchorBrowserTest::SetUpCommandLine(command_line); command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures, "ForceLoadAtTop"); } }; // Test that scroll restoration is disabled with ForceLoadAtTop IN_PROC_BROWSER_TEST_F(ForceLoadAtTopBrowserTest, ScrollRestorationDisabled) { GURL url( embedded_test_server()->GetURL("/scrollable_page_with_content.html")); WebContents* main_contents = shell()->web_contents(); RenderFrameSubmissionObserver frame_observer(main_contents); EXPECT_TRUE(NavigateToURL(shell(), url)); EXPECT_TRUE(WaitForRenderFrameReady(main_contents->GetMainFrame())); // Scroll down the page a bit EXPECT_TRUE(ExecuteScript(main_contents, "window.scrollTo(0, 1000)")); frame_observer.WaitForScrollOffsetAtTop(false); // Navigate away EXPECT_TRUE(ExecuteScript(main_contents, "window.location = 'about:blank'")); EXPECT_TRUE(WaitForLoadStop(main_contents)); EXPECT_TRUE(WaitForRenderFrameReady(main_contents->GetMainFrame())); // Navigate back EXPECT_TRUE(ExecuteScript(main_contents, "history.back()")); EXPECT_TRUE(WaitForLoadStop(main_contents)); EXPECT_TRUE(WaitForRenderFrameReady(main_contents->GetMainFrame())); // Wait a short amount of time to ensure the page does not scroll. base::RunLoop run_loop; base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); run_loop.Run(); RunUntilInputProcessed(RenderWidgetHostImpl::From( main_contents->GetRenderViewHost()->GetWidget())); EXPECT_TRUE(main_contents->GetMainFrame()->GetView()->IsScrollOffsetAtTop()); } // Test that element fragment anchor scrolling is disabled with ForceLoadAtTop IN_PROC_BROWSER_TEST_F(ForceLoadAtTopBrowserTest, FragmentAnchorDisabled) { GURL url(embedded_test_server()->GetURL( "/scrollable_page_with_content.html#text")); WebContents* main_contents = shell()->web_contents(); EXPECT_TRUE(NavigateToURL(shell(), url)); EXPECT_TRUE(WaitForRenderFrameReady(main_contents->GetMainFrame())); // Wait a short amount of time to ensure the page does not scroll. base::RunLoop run_loop; base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); run_loop.Run(); RunUntilInputProcessed(RenderWidgetHostImpl::From( main_contents->GetRenderViewHost()->GetWidget())); EXPECT_TRUE(main_contents->GetMainFrame()->GetView()->IsScrollOffsetAtTop()); } IN_PROC_BROWSER_TEST_F(ForceLoadAtTopBrowserTest, SameDocumentNavigation) { GURL url( embedded_test_server()->GetURL("/scrollable_page_with_content.html")); WebContents* main_contents = shell()->web_contents(); EXPECT_TRUE(NavigateToURL(shell(), url)); EXPECT_TRUE(WaitForRenderFrameReady(main_contents->GetMainFrame())); EXPECT_TRUE(main_contents->GetMainFrame()->GetView()->IsScrollOffsetAtTop()); ClickElementWithId(main_contents, "link"); RunUntilInputProcessed(GetWidgetHost()); EXPECT_FALSE(main_contents->GetMainFrame()->GetView()->IsScrollOffsetAtTop()); } IN_PROC_BROWSER_TEST_F(ForceLoadAtTopBrowserTest, TextFragmentAnchorDisabled) { GURL url(embedded_test_server()->GetURL( "/scrollable_page_with_content.html#:~:text=text")); WebContents* main_contents = shell()->web_contents(); RenderFrameSubmissionObserver frame_observer(main_contents); EXPECT_TRUE(NavigateToURL(shell(), url)); EXPECT_TRUE(WaitForRenderFrameReady(main_contents->GetMainFrame())); // Wait a short amount of time to ensure the page does not scroll. base::RunLoop run_loop; base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( FROM_HERE, run_loop.QuitClosure(), TestTimeouts::tiny_timeout()); run_loop.Run(); RunUntilInputProcessed(RenderWidgetHostImpl::From( main_contents->GetRenderViewHost()->GetWidget())); EXPECT_TRUE(main_contents->GetMainFrame()->GetView()->IsScrollOffsetAtTop()); } // Test that Tab key press puts focus from the start of selection. IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, TabFocus) { ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL( "/scrollable_page_with_anchor.html#:~:text=text")); WebContents* main_contents = shell()->web_contents(); RenderFrameSubmissionObserver frame_observer(main_contents); EXPECT_TRUE(NavigateToURL(shell(), url)); WaitForPageLoad(main_contents); frame_observer.WaitForScrollOffsetAtTop( /*expected_scroll_offset_at_top=*/false); DOMMessageQueue msg_queue; SimulateKeyPress(main_contents, ui::DomKey::TAB, ui::DomCode::TAB, ui::VKEY_TAB, false, false, false, false); // Wait for focus to happen. std::string message; EXPECT_TRUE(msg_queue.WaitForMessage(&message)); EXPECT_EQ("\"FocusDone2\"", message); } } // namespace content