// 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