// Copyright 2014 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/bind.h" #include "base/strings/stringprintf.h" #include "content/browser/frame_host/frame_tree.h" #include "content/browser/frame_host/navigation_controller_impl.h" #include "content/browser/frame_host/navigation_entry_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/browser/web_contents_observer.h" #include "content/public/common/bindings_policy.h" #include "content/public/common/url_constants.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/test_navigation_observer.h" #include "content/public/test/test_utils.h" #include "content/shell/browser/shell.h" #include "content/test/content_browser_test_utils_internal.h" #include "net/dns/mock_host_resolver.h" #include "net/test/embedded_test_server/embedded_test_server.h" namespace content { class NavigationControllerBrowserTest : public ContentBrowserTest { protected: void SetUpOnMainThread() override { host_resolver()->AddRule("*", "127.0.0.1"); ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady()); } }; IN_PROC_BROWSER_TEST_F(NavigationControllerBrowserTest, LoadDataWithBaseURL) { const GURL base_url("http://baseurl"); const GURL history_url("http://historyurl"); const std::string data = "foo"; const NavigationController& controller = shell()->web_contents()->GetController(); // Load data. Blocks until it is done. content::LoadDataWithBaseURL(shell(), history_url, data, base_url); // We should use history_url instead of the base_url as the original url of // this navigation entry, because base_url is only used for resolving relative // paths in the data, or enforcing same origin policy. EXPECT_EQ(controller.GetVisibleEntry()->GetOriginalRequestURL(), history_url); } // The renderer uses the position in the history list as a clue to whether a // navigation is stale. In the case where the entry limit is reached and the // history list is pruned, make sure that there is no mismatch that would cause // it to start incorrectly rejecting navigations as stale. See // http://crbug.com/89798. IN_PROC_BROWSER_TEST_F(NavigationControllerBrowserTest, DontIgnoreBackAfterNavEntryLimit) { NavigationController& controller = shell()->web_contents()->GetController(); const int kMaxEntryCount = static_cast(NavigationControllerImpl::max_entry_count()); // Load up to the max count, all entries should be there. for (int url_index = 0; url_index < kMaxEntryCount; ++url_index) { GURL url(base::StringPrintf("data:text/html,page%d", url_index)); EXPECT_TRUE(NavigateToURL(shell(), url)); } EXPECT_EQ(controller.GetEntryCount(), kMaxEntryCount); // Navigate twice more more. for (int url_index = kMaxEntryCount; url_index < kMaxEntryCount + 2; ++url_index) { GURL url(base::StringPrintf("data:text/html,page%d", url_index)); EXPECT_TRUE(NavigateToURL(shell(), url)); } // We expect page0 and page1 to be gone. EXPECT_EQ(kMaxEntryCount, controller.GetEntryCount()); EXPECT_EQ(GURL("data:text/html,page2"), controller.GetEntryAtIndex(0)->GetURL()); // Now try to go back. This should not hang. ASSERT_TRUE(controller.CanGoBack()); controller.GoBack(); EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); // This should have successfully gone back. EXPECT_EQ(GURL(base::StringPrintf("data:text/html,page%d", kMaxEntryCount)), controller.GetLastCommittedEntry()->GetURL()); } namespace { int RendererHistoryLength(Shell* shell) { int value = 0; EXPECT_TRUE(ExecuteScriptAndExtractInt( shell->web_contents(), "domAutomationController.send(history.length)", &value)); return value; } // Similar to the ones from content_browser_test_utils. bool NavigateToURLAndReplace(Shell* shell, const GURL& url) { WebContents* web_contents = shell->web_contents(); WaitForLoadStop(web_contents); TestNavigationObserver same_tab_observer(web_contents, 1); NavigationController::LoadURLParams params(url); params.should_replace_current_entry = true; web_contents->GetController().LoadURLWithParams(params); web_contents->Focus(); same_tab_observer.Wait(); if (!IsLastCommittedEntryOfPageType(web_contents, PAGE_TYPE_NORMAL)) return false; return web_contents->GetLastCommittedURL() == url; } } // namespace // When loading a new page to replace an old page in the history list, make sure // that the browser and renderer agree, and that both get it right. IN_PROC_BROWSER_TEST_F(NavigationControllerBrowserTest, CorrectLengthWithCurrentItemReplacement) { NavigationController& controller = shell()->web_contents()->GetController(); EXPECT_TRUE(NavigateToURL(shell(), GURL("data:text/html,page1"))); EXPECT_EQ(1, controller.GetEntryCount()); EXPECT_EQ(1, RendererHistoryLength(shell())); EXPECT_TRUE(NavigateToURLAndReplace(shell(), GURL("data:text/html,page2"))); EXPECT_EQ(1, controller.GetEntryCount()); EXPECT_EQ(1, RendererHistoryLength(shell())); // Note that there's no way to access the renderer's notion of the history // offset via JavaScript. Checking just the history length, though, is enough; // if the replacement failed, there would be a new history entry and thus an // incorrect length. } // When spawning a new page from a WebUI page, make sure that the browser and // renderer agree about the length of the history list, and that both get it // right. IN_PROC_BROWSER_TEST_F(NavigationControllerBrowserTest, CorrectLengthWithNewTabNavigatingFromWebUI) { GURL web_ui_page(std::string(kChromeUIScheme) + "://" + std::string(kChromeUIGpuHost)); EXPECT_TRUE(NavigateToURL(shell(), web_ui_page)); EXPECT_EQ(BINDINGS_POLICY_WEB_UI, shell()->web_contents()->GetRenderViewHost()->GetEnabledBindings()); ShellAddedObserver observer; std::string page_url = embedded_test_server()->GetURL( "/navigation_controller/simple_page_1.html").spec(); EXPECT_TRUE(ExecuteScript(shell()->web_contents(), "window.open('" + page_url + "', '_blank')")); Shell* shell2 = observer.GetShell(); WaitForLoadStop(shell2->web_contents()); EXPECT_EQ(1, shell2->web_contents()->GetController().GetEntryCount()); EXPECT_EQ(1, RendererHistoryLength(shell2)); // Again, as above, there's no way to access the renderer's notion of the // history offset via JavaScript. Checking just the history length, again, // will have to suffice. } namespace { struct FrameNavigateParamsCapturer : public WebContentsObserver { public: // Observes navigation for the specified |node|. explicit FrameNavigateParamsCapturer(FrameTreeNode* node) : WebContentsObserver( node->current_frame_host()->delegate()->GetAsWebContents()), frame_tree_node_id_(node->frame_tree_node_id()), message_loop_runner_(new MessageLoopRunner) {} void Wait() { message_loop_runner_->Run(); } const FrameNavigateParams& params() const { return params_; } const LoadCommittedDetails& details() const { return details_; } private: void DidNavigateAnyFrame(RenderFrameHost* render_frame_host, const LoadCommittedDetails& details, const FrameNavigateParams& params) override { RenderFrameHostImpl* rfh = static_cast(render_frame_host); if (rfh->frame_tree_node()->frame_tree_node_id() != frame_tree_node_id_) return; params_ = params; details_ = details; message_loop_runner_->Quit(); } // The id of the FrameTreeNode whose navigations to observe. int frame_tree_node_id_; // The params of the last navigation. FrameNavigateParams params_; // The details of the last navigation. LoadCommittedDetails details_; // The MessageLoopRunner used to spin the message loop. scoped_refptr message_loop_runner_; }; struct LoadCommittedCapturer : public WebContentsObserver { public: // Observes the load commit for the specified |node|. explicit LoadCommittedCapturer(FrameTreeNode* node) : WebContentsObserver( node->current_frame_host()->delegate()->GetAsWebContents()), frame_tree_node_id_(node->frame_tree_node_id()), message_loop_runner_(new MessageLoopRunner) {} // Observes the load commit for the next created frame in the specified // |web_contents|. explicit LoadCommittedCapturer(WebContents* web_contents) : WebContentsObserver(web_contents), frame_tree_node_id_(0), message_loop_runner_(new MessageLoopRunner) {} void Wait() { message_loop_runner_->Run(); } ui::PageTransition transition_type() const { return transition_type_; } private: void RenderFrameCreated(RenderFrameHost* render_frame_host) override { // If this object was created with a specified tree frame node, there // shouldn't be any frames being created. DCHECK_EQ(0, frame_tree_node_id_); RenderFrameHostImpl* rfh = static_cast(render_frame_host); frame_tree_node_id_ = rfh->frame_tree_node()->frame_tree_node_id(); } void DidCommitProvisionalLoadForFrame( RenderFrameHost* render_frame_host, const GURL& url, ui::PageTransition transition_type) override { DCHECK_NE(0, frame_tree_node_id_); RenderFrameHostImpl* rfh = static_cast(render_frame_host); if (rfh->frame_tree_node()->frame_tree_node_id() != frame_tree_node_id_) return; transition_type_ = transition_type; message_loop_runner_->Quit(); } // The id of the FrameTreeNode whose navigations to observe. int frame_tree_node_id_; // The transition_type of the last navigation. ui::PageTransition transition_type_; // The MessageLoopRunner used to spin the message loop. scoped_refptr message_loop_runner_; }; } // namespace // Verify that the distinction between manual and auto subframes is properly set // for subframe navigations. TODO(avi): It's rather bogus that the same info is // in two different enums; http://crbug.com/453555. IN_PROC_BROWSER_TEST_F(NavigationControllerBrowserTest, NavigationTypeClassification_NewAndAutoSubframe) { GURL main_url(embedded_test_server()->GetURL( "/navigation_controller/page_with_iframe.html")); NavigateToURL(shell(), main_url); // It is safe to obtain the root frame tree node here, as it doesn't change. FrameTreeNode* root = static_cast(shell()->web_contents())-> GetFrameTree()->root(); ASSERT_EQ(1U, root->child_count()); ASSERT_NE(nullptr, root->child_at(0)); { // Navigate the iframe to a new URL; expect a manual subframe transition. FrameNavigateParamsCapturer capturer(root->child_at(0)); GURL frame_url(embedded_test_server()->GetURL( "/navigation_controller/simple_page_1.html")); NavigateFrameToURL(root->child_at(0), frame_url); capturer.Wait(); EXPECT_EQ(ui::PAGE_TRANSITION_MANUAL_SUBFRAME, capturer.params().transition); EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.details().type); } { // Do a history navigation; expect an auto subframe transition. FrameNavigateParamsCapturer capturer(root->child_at(0)); shell()->web_contents()->GetController().GoBack(); capturer.Wait(); EXPECT_EQ(ui::PAGE_TRANSITION_AUTO_SUBFRAME, capturer.params().transition); EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.details().type); } { // Do a history navigation; expect an auto subframe transition. FrameNavigateParamsCapturer capturer(root->child_at(0)); shell()->web_contents()->GetController().GoForward(); capturer.Wait(); EXPECT_EQ(ui::PAGE_TRANSITION_AUTO_SUBFRAME, capturer.params().transition); EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.details().type); } { // Navigate the iframe to a new URL; expect a manual subframe transition. FrameNavigateParamsCapturer capturer(root->child_at(0)); GURL frame_url(embedded_test_server()->GetURL( "/navigation_controller/simple_page_2.html")); NavigateFrameToURL(root->child_at(0), frame_url); capturer.Wait(); EXPECT_EQ(ui::PAGE_TRANSITION_MANUAL_SUBFRAME, capturer.params().transition); EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.details().type); } { // Use location.assign(); expect a manual subframe transition. FrameNavigateParamsCapturer capturer(root->child_at(0)); GURL frame_url(embedded_test_server()->GetURL( "/navigation_controller/simple_page_1.html")); std::string script = "location.assign('" + frame_url.spec() + "')"; EXPECT_TRUE(content::ExecuteScript(root->child_at(0)->current_frame_host(), script)); capturer.Wait(); EXPECT_EQ(ui::PAGE_TRANSITION_MANUAL_SUBFRAME, capturer.params().transition); EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.details().type); } { // Use location.replace(); expect an auto subframe transition. (Replacements // aren't "navigation" so we only see the frame load committing.) LoadCommittedCapturer capturer(root->child_at(0)); GURL frame_url(embedded_test_server()->GetURL( "/navigation_controller/simple_page_2.html")); std::string script = "location.replace('" + frame_url.spec() + "')"; EXPECT_TRUE(content::ExecuteScript(root->child_at(0)->current_frame_host(), script)); capturer.Wait(); EXPECT_EQ(ui::PAGE_TRANSITION_AUTO_SUBFRAME, capturer.transition_type()); } { // Use history.pushState(); expect a manual subframe transition. FrameNavigateParamsCapturer capturer(root->child_at(0)); std::string script = "history.pushState({}, 'page 1', 'simple_page_1.html')"; EXPECT_TRUE(content::ExecuteScript(root->child_at(0)->current_frame_host(), script)); capturer.Wait(); EXPECT_EQ(ui::PAGE_TRANSITION_MANUAL_SUBFRAME, capturer.params().transition); EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.details().type); } { // Use history.replaceState(); expect an auto subframe transition. // (Replacements aren't "navigation" so we only see the frame load // committing.) LoadCommittedCapturer capturer(root->child_at(0)); std::string script = "history.replaceState({}, 'page 2', 'simple_page_2.html')"; EXPECT_TRUE(content::ExecuteScript(root->child_at(0)->current_frame_host(), script)); capturer.Wait(); EXPECT_EQ(ui::PAGE_TRANSITION_AUTO_SUBFRAME, capturer.transition_type()); } { // Reload the subframe; expect an auto subframe transition. (Reloads aren't // "navigation" so we only see the frame load committing.) LoadCommittedCapturer capturer(root->child_at(0)); EXPECT_TRUE(content::ExecuteScript(root->child_at(0)->current_frame_host(), "location.reload()")); capturer.Wait(); EXPECT_EQ(ui::PAGE_TRANSITION_AUTO_SUBFRAME, capturer.transition_type()); } { // Create an iframe; expect an auto subframe transition. (Initial frame // creation isn't "navigation" so we only see the frame load committing.) LoadCommittedCapturer capturer(shell()->web_contents()); GURL frame_url(embedded_test_server()->GetURL( "/navigation_controller/simple_page_1.html")); std::string script = "var iframe = document.createElement('iframe');" "iframe.src = '" + frame_url.spec() + "';" "document.body.appendChild(iframe);"; EXPECT_TRUE(content::ExecuteScript(root->current_frame_host(), script)); capturer.Wait(); EXPECT_EQ(ui::PAGE_TRANSITION_AUTO_SUBFRAME, capturer.transition_type()); } } } // namespace content