// 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 "build/build_config.h" #include "content/browser/renderer_host/frame_tree_node.h" #include "content/browser/renderer_host/render_widget_host_impl.h" #include "content/browser/web_contents/web_contents_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/back_forward_cache_util.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 "third_party/blink/public/common/switches.h" #include "url/gurl.h" // RunUntilInputProcessed will force a Blink lifecycle which is needed // because did_scroll is set in an onscroll handler which may be delayed from // the scroll by a frame. #define EXPECT_DID_SCROLL(scrolled) \ RunUntilInputProcessed(GetWidgetHost()); \ EXPECT_EQ(scrolled, EvalJs(main_contents, "did_scroll;", \ EXECUTE_SCRIPT_NO_USER_GESTURE)); #define ASSERT_DID_SCROLL(scrolled) \ RunUntilInputProcessed(GetWidgetHost()); \ ASSERT_EQ(scrolled, EvalJs(main_contents, "did_scroll;", \ EXECUTE_SCRIPT_NO_USER_GESTURE)); 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"); // Slow bots are flaky due to slower loading interacting with // deferred commits. command_line->AppendSwitch(blink::switches::kAllowPreCommitInput); } // 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() ->GetMainFrame() ->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); // 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()); // Observe the frame after page is loaded. Note that we need to initialize // this after navigation because the main RenderFrameHost might have changed // from before the navigation started. RenderFrameSubmissionObserver frame_observer(main_contents); WaitForPageLoad(main_contents); frame_observer.WaitForScrollOffsetAtTop( /*expected_scroll_offset_at_top=*/false); EXPECT_DID_SCROLL(true); } 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); EXPECT_DID_SCROLL(true); } 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); EXPECT_TRUE( ExecJs(main_contents, "location = '" + target_text_url.spec() + "';")); observer.Wait(); EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL()); // Observe the frame after page is loaded. Note that we need to initialize // this after navigation because the main RenderFrameHost might have changed // from before the navigation started. RenderFrameSubmissionObserver frame_observer(main_contents); WaitForPageLoad(main_contents); frame_observer.WaitForScrollOffsetAtTop( /*expected_scroll_offset_at_top=*/false); EXPECT_DID_SCROLL(true); } // Ensures that a simulated redirect service works correctly. That is, only the // initial NavigateToURL has a user gesture but this should be propagated // through the window.location navigation which doesn't have a user gesture. IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, UserGesturePassedThroughRedirect) { 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")); // This navigtion is simulated as if it came from the omnibox, hence it is // considered to be user initiated. EXPECT_TRUE(NavigateToURL(shell(), url)); WebContents* main_contents = shell()->web_contents(); TestNavigationObserver observer(main_contents); // This navigation occurs without a user gesture, simulating a client // redirect. However, because the above navigation didn't activate a text // fragment, permission should be propagated to this navigation. EXPECT_TRUE(ExecJs(main_contents, "location = '" + target_text_url.spec() + "';", EXECUTE_SCRIPT_NO_USER_GESTURE)); observer.Wait(); EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL()); WaitForPageLoad(main_contents); RenderFrameSubmissionObserver frame_observer(main_contents); frame_observer.WaitForScrollOffsetAtTop( /*expected_scroll_offset_at_top=*/false); EXPECT_DID_SCROLL(true); } // Ensures that a text fragment activation consumes a user gesture so that // future navigations cannot activate a text fragment without a new user // gesture. IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, UserGestureConsumed) { ASSERT_TRUE(embedded_test_server()->Start()); GURL empty_page_url(embedded_test_server()->GetURL("/empty.html")); GURL target_text_url(embedded_test_server()->GetURL( "/scrollable_page_with_content.html#:~:text=text")); WebContents* main_contents = shell()->web_contents(); // This navigtion is simulated as if it came from the omnibox, hence it is // considered to be user initiated. { TestNavigationObserver observer(main_contents); ASSERT_TRUE(NavigateToURL(shell(), target_text_url)); observer.Wait(); ASSERT_EQ(target_text_url, main_contents->GetLastCommittedURL()); // Ensure the page did scroll to the text fragment. Note, we can't use // WaitForPageLoad since the WaitForRenderFrameReady executes javascript // with a user gesture. WaitForLoadStop(main_contents); RenderFrameSubmissionObserver frame_observer(main_contents); frame_observer.WaitForScrollOffsetAtTop( /*expected_scroll_offset_at_top=*/false); ASSERT_DID_SCROLL(true); } // We now want to try a second text fragment navigation. Same document // navigations are blocked so we'll navigate away first. { TestNavigationObserver observer(main_contents); ASSERT_TRUE(ExecJs(main_contents, "location = '" + empty_page_url.spec() + "';", EXECUTE_SCRIPT_NO_USER_GESTURE)); observer.Wait(); ASSERT_EQ(empty_page_url, main_contents->GetLastCommittedURL()); WaitForLoadStop(main_contents); } // Now try another text fragment navigation. Since we haven't had a user // gesture since the last one, it should be blocked. { TestNavigationObserver observer(main_contents); ASSERT_TRUE(ExecJs(main_contents, "location = '" + target_text_url.spec() + "';", EXECUTE_SCRIPT_NO_USER_GESTURE)); observer.Wait(); ASSERT_EQ(target_text_url, main_contents->GetLastCommittedURL()); WaitForLoadStop(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(); EXPECT_DID_SCROLL(false); } } 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(); // The test assumes the previous page gets deleted after navigation and will // be recreated with did_scroll == false. Disable back/forward cache to ensure // that it doesn't get preserved in the cache. DisableBackForwardCacheForTesting( main_contents, BackForwardCacheImpl::TEST_ASSUMES_NO_CACHING); { // The RenderFrameSubmissionObserver destructor expects the RenderFrameHost // stays the same until it gets destructed, so we need to scope this to make // sure it gets destructed before the next navigation. 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(ExecJs(main_contents, "window.scrollTo(0, 0)")); frame_observer.WaitForScrollOffsetAtTop(true); } EXPECT_TRUE(NavigateToURL(shell(), url)); TestNavigationObserver observer(main_contents); EXPECT_TRUE( ExecJs(main_contents, "history.back()", EXECUTE_SCRIPT_NO_USER_GESTURE)); 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(); // 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_DID_SCROLL(false); } // Ensure a same-document navigation from browser UI scrolls to the text // fragment. IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, SameDocumentBrowserNavigation) { 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(ExecJs(main_contents, "window.scrollTo(0, 0)")); frame_observer.WaitForScrollOffsetAtTop(true); RunUntilInputProcessed(GetWidgetHost()); 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); EXPECT_DID_SCROLL(true); } IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, SameDocumentBrowserNavigationOnScriptNavigatedDocument) { ASSERT_TRUE(embedded_test_server()->Start()); WebContents* main_contents = shell()->web_contents(); RenderFrameSubmissionObserver frame_observer(main_contents); // The test assumes the RenderWidgetHost stays the same after navigation, // which won't happen if same-site back/forward-cache is enabled. Disable it // so that we will keep RenderWidgetHost even after navigation. DisableBackForwardCacheForTesting( main_contents, BackForwardCacheImpl::TEST_ASSUMES_NO_RENDER_FRAME_CHANGE); // Load an initial page { GURL initial_url(embedded_test_server()->GetURL("/empty.html")); EXPECT_TRUE(NavigateToURL(shell(), initial_url)); WaitForPageLoad(main_contents); } // Now navigate to the target document without a user gesture. We provide a // text-fragment here and expect it to be invoked because the initial load // was browser-initiated so its transferred to this load via the text fragment // token. This navigation ensures the token is consumed. { GURL target_url(embedded_test_server()->GetURL( "/scrollable_page_with_content.html#:~:text=text")); TestNavigationObserver observer(main_contents); EXPECT_TRUE(ExecJs(main_contents, "location = '" + target_url.spec() + "';", EXECUTE_SCRIPT_NO_USER_GESTURE)); observer.Wait(); EXPECT_EQ(target_url, main_contents->GetLastCommittedURL()); frame_observer.WaitForScrollOffsetAtTop(false); EXPECT_DID_SCROLL(true); } // 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(ExecJs(main_contents, "window.scrollTo(0, 0)")); frame_observer.WaitForScrollOffsetAtTop(true); RunUntilInputProcessed(GetWidgetHost()); EXPECT_TRUE(ExecJs(main_contents, "did_scroll = false;")); } // Perform a same-document browser initiated navigation. This should cause a // scroll because the navigation is browser-initiated, despite the fact that // the document was loaded without a user gesture. { 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); EXPECT_DID_SCROLL(true); } } // Ensure a text fragment token isn't generated via history.back() navigation. // This is a tricky case because all history navigations (including script // initiated) appear to the renderer as "browser-initiated". IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, HistoryDoesntGenerateToken) { ASSERT_TRUE(embedded_test_server()->Start()); WebContents* main_contents = shell()->web_contents(); GURL url(embedded_test_server()->GetURL( "a.com", "/scrollable_page_with_content.html#:~:text=text")); { // RenderFrameSubmissionObserver must not outlive the RenderWidgetHostImpl // so ensure it's destructed before we navigate to a new page. RenderFrameSubmissionObserver frame_observer(main_contents); // Load a page with a text-fragment 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(ExecJs(main_contents, "window.scrollTo(0, 0)", EXECUTE_SCRIPT_NO_USER_GESTURE)); frame_observer.WaitForScrollOffsetAtTop(true); RunUntilInputProcessed(GetWidgetHost()); EXPECT_TRUE(ExecJs(main_contents, "did_scroll = false;", EXECUTE_SCRIPT_NO_USER_GESTURE)); // Perform a scripted same-document navigation to a non-existent fragment to // generate a history entry. { GURL temp_url(embedded_test_server()->GetURL( "a.com", "/scrollable_page_with_content.html#doesntexist")); TestNavigationObserver observer(main_contents); EXPECT_TRUE(ExecJs(main_contents, JsReplace("location = $1;", temp_url), EXECUTE_SCRIPT_NO_USER_GESTURE)); observer.Wait(); EXPECT_EQ(temp_url, main_contents->GetLastCommittedURL()); } // Navigate back using history.back(). { TestNavigationObserver observer(main_contents); EXPECT_TRUE(ExecJs(main_contents, "history.back();", EXECUTE_SCRIPT_NO_USER_GESTURE)); observer.Wait(); EXPECT_EQ(url, main_contents->GetLastCommittedURL()); // The page should be restored to where we left off at the top. RunUntilInputProcessed(GetWidgetHost()); ASSERT_TRUE( frame_observer.LastRenderFrameMetadata().is_scroll_offset_at_top); ASSERT_DID_SCROLL(false); } } // Now try to navigate to a new page with a text-fragment. This should be // blocked because the token was consumed in the initial load at the top and // a new one should not have been generated by the same document navigations // above. { GURL new_url(embedded_test_server()->GetURL( "b.com", "/scrollable_page_with_content.html#:~:text=Some")); TestNavigationObserver observer(main_contents); EXPECT_TRUE(ExecJs(main_contents, "location = '" + new_url.spec() + "';", EXECUTE_SCRIPT_NO_USER_GESTURE)); observer.Wait(); RenderFrameSubmissionObserver frame_observer(main_contents); EXPECT_EQ(new_url, main_contents->GetLastCommittedURL()); frame_observer.WaitForScrollOffsetAtTop(true); EXPECT_DID_SCROLL(false); } } // Ensure same-document navigation to a text-fragment works when initiated from // the document itself. IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, SameDocumentScriptNavigation) { 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); // User gesture not required since the script is running in the same origin // as the page. EXPECT_TRUE(ExecJs(main_contents, "location = '" + target_text_url.spec() + "';", EXECUTE_SCRIPT_NO_USER_GESTURE)); observer.Wait(); EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL()); WaitForPageLoad(main_contents); RenderFrameSubmissionObserver frame_observer(main_contents); frame_observer.WaitForScrollOffsetAtTop( /*expected_scroll_offset_at_top=*/false); EXPECT_DID_SCROLL(true); } // Ensure same-document navigation to a text-fragment works when initiated from // the same origin. IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, SameDocumentScriptNavigationSameOrigin) { ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL( "a.com", "/scrollable_page_with_content.html")); GURL target_text_url(embedded_test_server()->GetURL( "a.com", "/scrollable_page_with_content.html#:~:text=some")); GURL cross_origin_inner_url( embedded_test_server()->GetURL("a.com", "/hello.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); WebContentsImpl* main_contents = static_cast(shell()->web_contents()); FrameTreeNode* root = main_contents->GetFrameTree()->root(); // Insert a same-origin iframe from which we'll execute script. { const auto script = JsReplace( R"JS( let f = document.createElement("iframe"); f.src=$1; document.body.appendChild(f); )JS", cross_origin_inner_url); TestNavigationObserver observer(main_contents); EXPECT_TRUE(ExecJs(main_contents, script, EXECUTE_SCRIPT_NO_USER_GESTURE)); observer.Wait(); ASSERT_EQ(1u, root->child_count()); } // Try navigating the top frame to a same-document text fragment from inside // the iframe. This should be allowed, even without user-gesture, since it's // same-origin; script is able to see all its content anyway. { TestNavigationObserver observer(main_contents); RenderFrameHostImpl* child_rfh = root->child_at(0)->current_frame_host(); EXPECT_TRUE(ExecJs(child_rfh, JsReplace("window.top.location = $1;", target_text_url), EXECUTE_SCRIPT_NO_USER_GESTURE)); observer.Wait(); EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL()); RenderFrameSubmissionObserver frame_observer(main_contents); WaitForPageLoad(main_contents); frame_observer.WaitForScrollOffsetAtTop( /*expected_scroll_offset_at_top=*/false); EXPECT_DID_SCROLL(true); } } // Ensure same-document navigation to a text-fragment is blocked when initiated // from a different origin. IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, SameDocumentScriptNavigationCrossOrigin) { ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL( "a.com", "/scrollable_page_with_content.html")); GURL target_text_url(embedded_test_server()->GetURL( "a.com", "/scrollable_page_with_content.html#:~:text=some")); GURL cross_origin_inner_url( embedded_test_server()->GetURL("b.com", "/hello.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); WebContentsImpl* main_contents = static_cast(shell()->web_contents()); FrameTreeNode* root = main_contents->GetFrameTree()->root(); // Insert a cross-origin iframe from which we'll execute script. { const auto script = JsReplace( R"JS( let f = document.createElement("iframe"); f.src=$1; document.body.appendChild(f); )JS", cross_origin_inner_url); TestNavigationObserver observer(main_contents); EXPECT_TRUE(ExecJs(main_contents, script, EXECUTE_SCRIPT_NO_USER_GESTURE)); observer.Wait(); ASSERT_EQ(1u, root->child_count()); } // Try navigating the top frame to a same-document text fragment from inside // the iframe. This should be blocked as its cross-origin. Note, the script // executes with a user gesture but this is still blocked. Same-document // navigations are allowed only when initiated from same-origin or // browser-UI. { TestNavigationObserver observer(main_contents); RenderFrameHostImpl* child_rfh = root->child_at(0)->current_frame_host(); EXPECT_TRUE(ExecJs( child_rfh, JsReplace("window.top.location = $1;", target_text_url))); observer.Wait(); EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL()); WaitForPageLoad(main_contents); EXPECT_DID_SCROLL(false); } } 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: force-load-at-top=?0\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); EXPECT_DID_SCROLL(true); } 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(); EXPECT_DID_SCROLL(false); } // 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); } class ForceLoadAtTopBrowserTest : public TextFragmentAnchorBrowserTest { protected: // Loads the given path as predetermined HTML response with a // |Document-Policy: force-load-at-top| header and waits for the navigation // to finish. void LoadScrollablePageWithContent(const std::string& path) { std::size_t hash_pos = path.find("#"); std::string path_without_fragment = path; if (hash_pos != std::string::npos) { path_without_fragment = path.substr(0, hash_pos); } net::test_server::ControllableHttpResponse response(embedded_test_server(), path_without_fragment); ASSERT_TRUE(embedded_test_server()->Start()); GURL url(embedded_test_server()->GetURL(path)); RenderFrameSubmissionObserver frame_observer(shell()->web_contents()); // Load the target document. TestNavigationManager navigation_manager(shell()->web_contents(), url); shell()->LoadURL(url); // Start navigation ASSERT_TRUE(navigation_manager.WaitForRequestStart()); navigation_manager.ResumeNavigation(); // Send Document-Policy header response.WaitForRequest(); std::string response_string = "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" R"HTML( Go Down

Some text

)HTML"; response.Send(response_string); response.Done(); ASSERT_TRUE(navigation_manager.WaitForResponse()); navigation_manager.ResumeNavigation(); navigation_manager.WaitForNavigationFinished(); } }; // Test that scroll restoration is disabled with ForceLoadAtTop IN_PROC_BROWSER_TEST_F(ForceLoadAtTopBrowserTest, ScrollRestorationDisabled) { ASSERT_NO_FATAL_FAILURE(LoadScrollablePageWithContent("/index.html")); WebContents* main_contents = shell()->web_contents(); // This test expects the document is freshly loaded on the back navigation // so that the document policy to force-load-at-top will run. This will not // happen if the document is back-forward cached, so we need to disable it. DisableBackForwardCacheForTesting(main_contents, BackForwardCache::TEST_ASSUMES_NO_CACHING); RenderFrameSubmissionObserver frame_observer(main_contents); EXPECT_TRUE(WaitForRenderFrameReady(main_contents->GetMainFrame())); // Scroll down the page a bit EXPECT_TRUE(ExecJs(main_contents, "window.scrollTo(0, 1000)")); frame_observer.WaitForScrollOffsetAtTop(false); // Navigate away EXPECT_TRUE(ExecJs(main_contents, "window.location = 'about:blank'")); EXPECT_TRUE(WaitForLoadStop(main_contents)); EXPECT_TRUE(WaitForRenderFrameReady(main_contents->GetMainFrame())); // Navigate back EXPECT_TRUE(ExecJs(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(GetWidgetHost()); const cc::RenderFrameMetadata& last_metadata = RenderFrameSubmissionObserver(main_contents).LastRenderFrameMetadata(); EXPECT_TRUE(last_metadata.is_scroll_offset_at_top); } // Test that element fragment anchor scrolling is disabled with ForceLoadAtTop IN_PROC_BROWSER_TEST_F(ForceLoadAtTopBrowserTest, FragmentAnchorDisabled) { ASSERT_NO_FATAL_FAILURE(LoadScrollablePageWithContent("/index.html#text")); WebContents* main_contents = shell()->web_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(GetWidgetHost()); const cc::RenderFrameMetadata& last_metadata = RenderFrameSubmissionObserver(main_contents).LastRenderFrameMetadata(); EXPECT_TRUE(last_metadata.is_scroll_offset_at_top); } IN_PROC_BROWSER_TEST_F(ForceLoadAtTopBrowserTest, SameDocumentNavigation) { ASSERT_NO_FATAL_FAILURE(LoadScrollablePageWithContent("/index.html")); WebContents* main_contents = shell()->web_contents(); EXPECT_TRUE(WaitForRenderFrameReady(main_contents->GetMainFrame())); { const cc::RenderFrameMetadata& last_metadata = RenderFrameSubmissionObserver(main_contents).LastRenderFrameMetadata(); EXPECT_TRUE(last_metadata.is_scroll_offset_at_top); } ClickElementWithId(main_contents, "link"); RunUntilInputProcessed(GetWidgetHost()); { const cc::RenderFrameMetadata& last_metadata = RenderFrameSubmissionObserver(main_contents).LastRenderFrameMetadata(); EXPECT_FALSE(last_metadata.is_scroll_offset_at_top); } } IN_PROC_BROWSER_TEST_F(ForceLoadAtTopBrowserTest, TextFragmentAnchorDisabled) { ASSERT_NO_FATAL_FAILURE( LoadScrollablePageWithContent("/index.html#:~:text=text")); WebContents* main_contents = shell()->web_contents(); RenderFrameSubmissionObserver frame_observer(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(GetWidgetHost()); const cc::RenderFrameMetadata& last_metadata = RenderFrameSubmissionObserver(main_contents).LastRenderFrameMetadata(); EXPECT_TRUE(last_metadata.is_scroll_offset_at_top); } } // namespace content