diff options
author | avi@chromium.org <avi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-04-10 21:26:37 +0000 |
---|---|---|
committer | avi@chromium.org <avi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-04-10 21:26:37 +0000 |
commit | f9e4daec200c22fb09df67bcd7f542a2d52fc9ed (patch) | |
tree | 5016666f8a1aea4d60dc911e1bcef4feef167e12 /content/browser/web_contents/navigation_controller_impl_unittest.cc | |
parent | e5e84e275e9ef65d280d66f2e8ee3ccb666a1cdb (diff) | |
download | chromium_src-f9e4daec200c22fb09df67bcd7f542a2d52fc9ed.zip chromium_src-f9e4daec200c22fb09df67bcd7f542a2d52fc9ed.tar.gz chromium_src-f9e4daec200c22fb09df67bcd7f542a2d52fc9ed.tar.bz2 |
TabContents -> WebContentsImpl, part 3.
BUG=105875
TEST=no change
Review URL: http://codereview.chromium.org/9960071
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@131634 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'content/browser/web_contents/navigation_controller_impl_unittest.cc')
-rw-r--r-- | content/browser/web_contents/navigation_controller_impl_unittest.cc | 2592 |
1 files changed, 2592 insertions, 0 deletions
diff --git a/content/browser/web_contents/navigation_controller_impl_unittest.cc b/content/browser/web_contents/navigation_controller_impl_unittest.cc new file mode 100644 index 0000000..023e83d --- /dev/null +++ b/content/browser/web_contents/navigation_controller_impl_unittest.cc @@ -0,0 +1,2592 @@ +// Copyright (c) 2012 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/file_util.h" +#include "base/memory/scoped_ptr.h" +#include "base/path_service.h" +#include "base/stl_util.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +// These are only used for commented out tests. If someone wants to enable +// them, they should be moved to chrome first. +// #include "chrome/browser/history/history.h" +// #include "chrome/browser/profiles/profile_manager.h" +// #include "chrome/browser/sessions/session_service.h" +// #include "chrome/browser/sessions/session_service_factory.h" +// #include "chrome/browser/sessions/session_service_test_helper.h" +// #include "chrome/browser/sessions/session_types.h" +#include "content/browser/renderer_host/test_render_view_host.h" +#include "content/browser/site_instance_impl.h" +#include "content/browser/tab_contents/tab_contents.h" +#include "content/browser/tab_contents/test_web_contents.h" +#include "content/browser/web_contents/navigation_controller_impl.h" +#include "content/browser/web_contents/navigation_entry_impl.h" +#include "content/common/view_messages.h" +#include "content/public/browser/navigation_details.h" +#include "content/public/browser/notification_registrar.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/web_contents_delegate.h" +#include "content/test/mock_render_process_host.h" +#include "content/test/test_browser_context.h" +#include "content/test/test_notification_tracker.h" +#include "net/base/net_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "webkit/glue/webkit_glue.h" + +using base::Time; +using content::NavigationController; +using content::NavigationEntry; +using content::NavigationEntryImpl; +using content::RenderViewHostImplTestHarness; +using content::SiteInstance; +using content::TestRenderViewHost; +using content::TestWebContents; +using content::WebContents; + +// NavigationControllerTest ---------------------------------------------------- + +class NavigationControllerTest : public RenderViewHostImplTestHarness { + public: + NavigationControllerTest() {} + + NavigationControllerImpl& controller_impl() { + return static_cast<NavigationControllerImpl&>(controller()); + } +}; + +void RegisterForAllNavNotifications(TestNotificationTracker* tracker, + NavigationController* controller) { + tracker->ListenFor(content::NOTIFICATION_NAV_ENTRY_COMMITTED, + content::Source<NavigationController>( + controller)); + tracker->ListenFor(content::NOTIFICATION_NAV_LIST_PRUNED, + content::Source<NavigationController>( + controller)); + tracker->ListenFor(content::NOTIFICATION_NAV_ENTRY_CHANGED, + content::Source<NavigationController>( + controller)); +} + +SiteInstance* GetSiteInstanceFromEntry(NavigationEntry* entry) { + return NavigationEntryImpl::FromNavigationEntry(entry)->site_instance(); +} + +class TestWebContentsDelegate : public content::WebContentsDelegate { + public: + explicit TestWebContentsDelegate() : + navigation_state_change_count_(0) {} + + int navigation_state_change_count() { + return navigation_state_change_count_; + } + + // Keep track of whether the tab has notified us of a navigation state change. + virtual void NavigationStateChanged(const WebContents* source, + unsigned changed_flags) { + navigation_state_change_count_++; + } + + private: + // The number of times NavigationStateChanged has been called. + int navigation_state_change_count_; +}; + +// ----------------------------------------------------------------------------- + +TEST_F(NavigationControllerTest, Defaults) { + NavigationControllerImpl& controller = controller_impl(); + + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.GetLastCommittedEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), -1); + EXPECT_EQ(controller.GetEntryCount(), 0); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +TEST_F(NavigationControllerTest, LoadURL) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + // Creating a pending notification should not have issued any of the + // notifications we're listening for. + EXPECT_EQ(0U, notifications.size()); + + // The load should now be pending. + EXPECT_EQ(controller.GetEntryCount(), 0); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), -1); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_FALSE(controller.GetLastCommittedEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(contents()->GetMaxPageID(), -1); + + // We should have gotten no notifications from the preceeding checks. + EXPECT_EQ(0U, notifications.size()); + + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // The load should now be committed. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(contents()->GetMaxPageID(), 0); + + // Load another... + controller.LoadURL( + url2, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + + // The load should now be pending. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + // TODO(darin): maybe this should really be true? + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(contents()->GetMaxPageID(), 0); + + // Simulate the beforeunload ack for the cross-site transition, and then the + // commit. + test_rvh()->SendShouldCloseACK(true); + static_cast<TestRenderViewHost*>( + contents()->GetPendingRenderViewHost())->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // The load should now be committed. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(contents()->GetMaxPageID(), 1); +} + +// Tests what happens when the same page is loaded again. Should not create a +// new session history entry. This is what happens when you press enter in the +// URL bar to reload: a pending entry is created and then it is discarded when +// the load commits (because WebCore didn't actually make a new entry). +TEST_F(NavigationControllerTest, LoadURL_SamePage) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // We should not have produced a new session history entry. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Tests loading a URL but discarding it before the load commits. +TEST_F(NavigationControllerTest, LoadURL_Discarded) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.LoadURL( + url2, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + controller.DiscardNonCommittedEntries(); + EXPECT_EQ(0U, notifications.size()); + + // Should not have produced a new session history entry. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Tests navigations that come in unrequested. This happens when the user +// navigates from the web page, and here we test that there is no pending entry. +TEST_F(NavigationControllerTest, LoadURL_NoPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // First make an existing committed entry. + const GURL kExistingURL1("http://eh"); + controller.LoadURL(kExistingURL1, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, kExistingURL1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Do a new navigation without making a pending one. + const GURL kNewURL("http://see"); + test_rvh()->SendNavigate(99, kNewURL); + + // There should no longer be any pending entry, and the third navigation we + // just made should be committed. + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kNewURL, controller.GetActiveEntry()->GetURL()); +} + +// Tests navigating to a new URL when there is a new pending navigation that is +// not the one that just loaded. This will happen if the user types in a URL to +// somewhere slow, and then navigates the current page before the typed URL +// commits. +TEST_F(NavigationControllerTest, LoadURL_NewPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // First make an existing committed entry. + const GURL kExistingURL1("http://eh"); + controller.LoadURL(kExistingURL1, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, kExistingURL1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Make a pending entry to somewhere new. + const GURL kExistingURL2("http://bee"); + controller.LoadURL(kExistingURL2, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + + // After the beforeunload but before it commits, do a new navigation. + test_rvh()->SendShouldCloseACK(true); + const GURL kNewURL("http://see"); + static_cast<TestRenderViewHost*>( + contents()->GetPendingRenderViewHost())->SendNavigate(3, kNewURL); + + // There should no longer be any pending entry, and the third navigation we + // just made should be committed. + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kNewURL, controller.GetActiveEntry()->GetURL()); +} + +// Tests navigating to a new URL when there is a pending back/forward +// navigation. This will happen if the user hits back, but before that commits, +// they navigate somewhere new. +TEST_F(NavigationControllerTest, LoadURL_ExistingPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // First make some history. + const GURL kExistingURL1("http://foo/eh"); + controller.LoadURL(kExistingURL1, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, kExistingURL1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + const GURL kExistingURL2("http://foo/bee"); + controller.LoadURL(kExistingURL2, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, kExistingURL2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Now make a pending back/forward navigation. The zeroth entry should be + // pending. + controller.GoBack(); + EXPECT_EQ(0U, notifications.size()); + EXPECT_EQ(0, controller.GetPendingEntryIndex()); + EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); + + // Before that commits, do a new navigation. + const GURL kNewURL("http://foo/see"); + content::LoadCommittedDetails details; + test_rvh()->SendNavigate(3, kNewURL); + + // There should no longer be any pending entry, and the third navigation we + // just made should be committed. + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(2, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kNewURL, controller.GetActiveEntry()->GetURL()); +} + +// Tests navigating to an existing URL when there is a pending new navigation. +// This will happen if the user enters a URL, but before that commits, the +// current page fires history.back(). +TEST_F(NavigationControllerTest, LoadURL_BackPreemptsPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // First make some history. + const GURL kExistingURL1("http://foo/eh"); + controller.LoadURL(kExistingURL1, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, kExistingURL1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + const GURL kExistingURL2("http://foo/bee"); + controller.LoadURL(kExistingURL2, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, kExistingURL2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Now make a pending new navigation. + const GURL kNewURL("http://foo/see"); + controller.LoadURL( + kNewURL, content::Referrer(), content::PAGE_TRANSITION_TYPED, + std::string()); + EXPECT_EQ(0U, notifications.size()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); + + // Before that commits, a back navigation from the renderer commits. + test_rvh()->SendNavigate(0, kExistingURL1); + + // There should no longer be any pending entry, and the back navigation we + // just made should be committed. + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kExistingURL1, controller.GetActiveEntry()->GetURL()); +} + +// Tests an ignored navigation when there is a pending new navigation. +// This will happen if the user enters a URL, but before that commits, the +// current blank page reloads. See http://crbug.com/77507. +TEST_F(NavigationControllerTest, LoadURL_IgnorePreemptsPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Set a WebContentsDelegate to listen for state changes. + scoped_ptr<TestWebContentsDelegate> delegate(new TestWebContentsDelegate()); + EXPECT_FALSE(contents()->GetDelegate()); + contents()->SetDelegate(delegate.get()); + + // Without any navigations, the renderer starts at about:blank. + const GURL kExistingURL("about:blank"); + + // Now make a pending new navigation. + const GURL kNewURL("http://eh"); + controller.LoadURL( + kNewURL, content::Referrer(), content::PAGE_TRANSITION_TYPED, + std::string()); + EXPECT_EQ(0U, notifications.size()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(-1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(1, delegate->navigation_state_change_count()); + + // Before that commits, a document.write and location.reload can cause the + // renderer to send a FrameNavigate with page_id -1. + test_rvh()->SendNavigate(-1, kExistingURL); + + // This should clear the pending entry and notify of a navigation state + // change, so that we do not keep displaying kNewURL. + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_EQ(-1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(2, delegate->navigation_state_change_count()); + + contents()->SetDelegate(NULL); +} + +// Tests that the pending entry state is correct after an abort. +TEST_F(NavigationControllerTest, LoadURL_AbortCancelsPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Set a WebContentsDelegate to listen for state changes. + scoped_ptr<TestWebContentsDelegate> delegate(new TestWebContentsDelegate()); + EXPECT_FALSE(contents()->GetDelegate()); + contents()->SetDelegate(delegate.get()); + + // Without any navigations, the renderer starts at about:blank. + const GURL kExistingURL("about:blank"); + + // Now make a pending new navigation. + const GURL kNewURL("http://eh"); + controller.LoadURL( + kNewURL, content::Referrer(), content::PAGE_TRANSITION_TYPED, + std::string()); + EXPECT_EQ(0U, notifications.size()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(-1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(1, delegate->navigation_state_change_count()); + + // It may abort before committing, if it's a download or due to a stop or + // a new navigation from the user. + ViewHostMsg_DidFailProvisionalLoadWithError_Params params; + params.frame_id = 1; + params.is_main_frame = true; + params.error_code = net::ERR_ABORTED; + params.error_description = string16(); + params.url = kNewURL; + params.showing_repost_interstitial = false; + test_rvh()->OnMessageReceived( + ViewHostMsg_DidFailProvisionalLoadWithError(0, // routing_id + params)); + + // This should clear the pending entry and notify of a navigation state + // change, so that we do not keep displaying kNewURL. + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_EQ(-1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(2, delegate->navigation_state_change_count()); + + contents()->SetDelegate(NULL); +} + +// Tests that the pending entry state is correct after a redirect and abort. +// http://crbug.com/83031. +TEST_F(NavigationControllerTest, LoadURL_RedirectAbortCancelsPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Set a WebContentsDelegate to listen for state changes. + scoped_ptr<TestWebContentsDelegate> delegate(new TestWebContentsDelegate()); + EXPECT_FALSE(contents()->GetDelegate()); + contents()->SetDelegate(delegate.get()); + + // Without any navigations, the renderer starts at about:blank. + const GURL kExistingURL("about:blank"); + + // Now make a pending new navigation. + const GURL kNewURL("http://eh"); + controller.LoadURL( + kNewURL, content::Referrer(), content::PAGE_TRANSITION_TYPED, + std::string()); + EXPECT_EQ(0U, notifications.size()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(-1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(1, delegate->navigation_state_change_count()); + + // Now the navigation redirects. + const GURL kRedirectURL("http://bee"); + test_rvh()->OnMessageReceived( + ViewHostMsg_DidRedirectProvisionalLoad(0, // routing_id + -1, // pending page_id + GURL(), // opener + kNewURL, // old url + kRedirectURL)); // new url + + // We don't want to change the NavigationEntry's url, in case it cancels. + // Prevents regression of http://crbug.com/77786. + EXPECT_EQ(kNewURL, controller.GetPendingEntry()->GetURL()); + + // It may abort before committing, if it's a download or due to a stop or + // a new navigation from the user. + ViewHostMsg_DidFailProvisionalLoadWithError_Params params; + params.frame_id = 1; + params.is_main_frame = true; + params.error_code = net::ERR_ABORTED; + params.error_description = string16(); + params.url = kRedirectURL; + params.showing_repost_interstitial = false; + test_rvh()->OnMessageReceived( + ViewHostMsg_DidFailProvisionalLoadWithError(0, // routing_id + params)); + + // This should clear the pending entry and notify of a navigation state + // change, so that we do not keep displaying kNewURL. + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_EQ(-1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(2, delegate->navigation_state_change_count()); + + contents()->SetDelegate(NULL); +} + +TEST_F(NavigationControllerTest, Reload) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + controller.GetActiveEntry()->SetTitle(ASCIIToUTF16("Title")); + controller.Reload(true); + EXPECT_EQ(0U, notifications.size()); + + // The reload is pending. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), 0); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + // Make sure the title has been cleared (will be redrawn just after reload). + // Avoids a stale cached title when the new page being reloaded has no title. + // See http://crbug.com/96041. + EXPECT_TRUE(controller.GetActiveEntry()->GetTitle().empty()); + + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Now the reload is committed. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Tests what happens when a reload navigation produces a new page. +TEST_F(NavigationControllerTest, Reload_GeneratesNewPage) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.Reload(true); + EXPECT_EQ(0U, notifications.size()); + + test_rvh()->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Now the reload is committed. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Tests what happens when we navigate back successfully +TEST_F(NavigationControllerTest, Back) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + const GURL url2("http://foo2"); + test_rvh()->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.GoBack(); + EXPECT_EQ(0U, notifications.size()); + + // We should now have a pending navigation to go back. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), 0); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_TRUE(controller.CanGoForward()); + + test_rvh()->SendNavigate(0, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // The back navigation completed successfully. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_TRUE(controller.CanGoForward()); +} + +// Tests what happens when a back navigation produces a new page. +TEST_F(NavigationControllerTest, Back_GeneratesNewPage) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.LoadURL( + url2, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.GoBack(); + EXPECT_EQ(0U, notifications.size()); + + // We should now have a pending navigation to go back. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), 0); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_TRUE(controller.CanGoForward()); + + test_rvh()->SendNavigate(2, url3); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // The back navigation resulted in a completely new navigation. + // TODO(darin): perhaps this behavior will be confusing to users? + EXPECT_EQ(controller.GetEntryCount(), 3); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 2); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Receives a back message when there is a new pending navigation entry. +TEST_F(NavigationControllerTest, Back_NewPending) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL kUrl1("http://foo1"); + const GURL kUrl2("http://foo2"); + const GURL kUrl3("http://foo3"); + + // First navigate two places so we have some back history. + test_rvh()->SendNavigate(0, kUrl1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // controller.LoadURL(kUrl2, content::PAGE_TRANSITION_TYPED); + test_rvh()->SendNavigate(1, kUrl2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Now start a new pending navigation and go back before it commits. + controller.LoadURL( + kUrl3, content::Referrer(), content::PAGE_TRANSITION_TYPED, + std::string()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(kUrl3, controller.GetPendingEntry()->GetURL()); + controller.GoBack(); + + // The pending navigation should now be the "back" item and the new one + // should be gone. + EXPECT_EQ(0, controller.GetPendingEntryIndex()); + EXPECT_EQ(kUrl1, controller.GetPendingEntry()->GetURL()); +} + +// Receives a back message when there is a different renavigation already +// pending. +TEST_F(NavigationControllerTest, Back_OtherBackPending) { + NavigationControllerImpl& controller = controller_impl(); + const GURL kUrl1("http://foo/1"); + const GURL kUrl2("http://foo/2"); + const GURL kUrl3("http://foo/3"); + + // First navigate three places so we have some back history. + test_rvh()->SendNavigate(0, kUrl1); + test_rvh()->SendNavigate(1, kUrl2); + test_rvh()->SendNavigate(2, kUrl3); + + // With nothing pending, say we get a navigation to the second entry. + test_rvh()->SendNavigate(1, kUrl2); + + // We know all the entries have the same site instance, so we can just grab + // a random one for looking up other entries. + SiteInstance* site_instance = + NavigationEntryImpl::FromNavigationEntry( + controller.GetLastCommittedEntry())->site_instance(); + + // That second URL should be the last committed and it should have gotten the + // new title. + EXPECT_EQ(kUrl2, controller.GetEntryWithPageID(site_instance, 1)->GetURL()); + EXPECT_EQ(1, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + + // Now go forward to the last item again and say it was committed. + controller.GoForward(); + test_rvh()->SendNavigate(2, kUrl3); + + // Now start going back one to the second page. It will be pending. + controller.GoBack(); + EXPECT_EQ(1, controller.GetPendingEntryIndex()); + EXPECT_EQ(2, controller.GetLastCommittedEntryIndex()); + + // Not synthesize a totally new back event to the first page. This will not + // match the pending one. + test_rvh()->SendNavigate(0, kUrl1); + + // The committed navigation should clear the pending entry. + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + + // But the navigated entry should be the last committed. + EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); + EXPECT_EQ(kUrl1, controller.GetLastCommittedEntry()->GetURL()); +} + +// Tests what happens when we navigate forward successfully. +TEST_F(NavigationControllerTest, Forward) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + test_rvh()->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.GoBack(); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.GoForward(); + + // We should now have a pending navigation to go forward. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), 1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + + test_rvh()->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // The forward navigation completed successfully. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Tests what happens when a forward navigation produces a new page. +TEST_F(NavigationControllerTest, Forward_GeneratesNewPage) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + const GURL url3("http://foo3"); + + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + test_rvh()->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.GoBack(); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + controller.GoForward(); + EXPECT_EQ(0U, notifications.size()); + + // Should now have a pending navigation to go forward. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_EQ(controller.GetPendingEntryIndex(), 1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + + test_rvh()->SendNavigate(2, url3); + EXPECT_TRUE(notifications.Check2AndReset( + content::NOTIFICATION_NAV_LIST_PRUNED, + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Two consequent navigation for the same URL entered in should be considered +// as SAME_PAGE navigation even when we are redirected to some other page. +TEST_F(NavigationControllerTest, Redirect) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); // Redirection target + + // First request + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Second request + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_EQ(url1, controller.GetActiveEntry()->GetURL()); + + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url2; + params.transition = content::PAGE_TRANSITION_SERVER_REDIRECT; + params.redirects.push_back(GURL("http://foo1")); + params.redirects.push_back(GURL("http://foo2")); + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + content::LoadCommittedDetails details; + + EXPECT_EQ(0U, notifications.size()); + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + EXPECT_TRUE(details.type == content::NAVIGATION_TYPE_SAME_PAGE); + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_EQ(url2, controller.GetActiveEntry()->GetURL()); + + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Similar to Redirect above, but the first URL is requested by POST, +// the second URL is requested by GET. NavigationEntry::has_post_data_ +// must be cleared. http://crbug.com/21245 +TEST_F(NavigationControllerTest, PostThenRedirect) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); // Redirection target + + // First request as POST + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + controller.GetActiveEntry()->SetHasPostData(true); + + EXPECT_EQ(0U, notifications.size()); + test_rvh()->SendNavigate(0, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Second request + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_EQ(url1, controller.GetActiveEntry()->GetURL()); + + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url2; + params.transition = content::PAGE_TRANSITION_SERVER_REDIRECT; + params.redirects.push_back(GURL("http://foo1")); + params.redirects.push_back(GURL("http://foo2")); + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + content::LoadCommittedDetails details; + + EXPECT_EQ(0U, notifications.size()); + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + EXPECT_TRUE(details.type == content::NAVIGATION_TYPE_SAME_PAGE); + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_EQ(url2, controller.GetActiveEntry()->GetURL()); + EXPECT_FALSE(controller.GetActiveEntry()->GetHasPostData()); + + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// A redirect right off the bat should be a NEW_PAGE. +TEST_F(NavigationControllerTest, ImmediateRedirect) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); // Redirection target + + // First request + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + + EXPECT_TRUE(controller.GetPendingEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_EQ(url1, controller.GetActiveEntry()->GetURL()); + + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url2; + params.transition = content::PAGE_TRANSITION_SERVER_REDIRECT; + params.redirects.push_back(GURL("http://foo1")); + params.redirects.push_back(GURL("http://foo2")); + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + content::LoadCommittedDetails details; + + EXPECT_EQ(0U, notifications.size()); + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + EXPECT_TRUE(details.type == content::NAVIGATION_TYPE_NEW_PAGE); + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_EQ(url2, controller.GetActiveEntry()->GetURL()); + + EXPECT_FALSE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +// Tests navigation via link click within a subframe. A new navigation entry +// should be created. +TEST_F(NavigationControllerTest, NewSubframe) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + const GURL url2("http://foo2"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 1; + params.url = url2; + params.transition = content::PAGE_TRANSITION_MANUAL_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + content::LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(url1, details.previous_url); + EXPECT_FALSE(details.is_in_page); + EXPECT_FALSE(details.is_main_frame); + + // The new entry should be appended. + EXPECT_EQ(2, controller.GetEntryCount()); + + // New entry should refer to the new page, but the old URL (entries only + // reflect the toplevel URL). + EXPECT_EQ(url1, details.entry->GetURL()); + EXPECT_EQ(params.page_id, details.entry->GetPageID()); +} + +// Some pages create a popup, then write an iframe into it. This causes a +// subframe navigation without having any committed entry. Such navigations +// just get thrown on the ground, but we shouldn't crash. +TEST_F(NavigationControllerTest, SubframeOnEmptyPage) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Navigation controller currently has no entries. + const GURL url("http://foo2"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 1; + params.url = url; + params.transition = content::PAGE_TRANSITION_AUTO_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url)); + + content::LoadCommittedDetails details; + EXPECT_FALSE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(0U, notifications.size()); +} + +// Auto subframes are ones the page loads automatically like ads. They should +// not create new navigation entries. +TEST_F(NavigationControllerTest, AutoSubframe) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + const GURL url2("http://foo2"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url2; + params.transition = content::PAGE_TRANSITION_AUTO_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + // Navigating should do nothing. + content::LoadCommittedDetails details; + EXPECT_FALSE(controller.RendererDidNavigate(params, &details)); + EXPECT_EQ(0U, notifications.size()); + + // There should still be only one entry. + EXPECT_EQ(1, controller.GetEntryCount()); +} + +// Tests navigation and then going back to a subframe navigation. +TEST_F(NavigationControllerTest, BackSubframe) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Main page. + const GURL url1("http://foo1"); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // First manual subframe navigation. + const GURL url2("http://foo2"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 1; + params.url = url2; + params.transition = content::PAGE_TRANSITION_MANUAL_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + // This should generate a new entry. + content::LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(2, controller.GetEntryCount()); + + // Second manual subframe navigation should also make a new entry. + const GURL url3("http://foo3"); + params.page_id = 2; + params.url = url3; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(3, controller.GetEntryCount()); + EXPECT_EQ(2, controller.GetCurrentEntryIndex()); + + // Go back one. + controller.GoBack(); + params.url = url2; + params.page_id = 1; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(3, controller.GetEntryCount()); + EXPECT_EQ(1, controller.GetCurrentEntryIndex()); + + // Go back one more. + controller.GoBack(); + params.url = url1; + params.page_id = 0; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(3, controller.GetEntryCount()); + EXPECT_EQ(0, controller.GetCurrentEntryIndex()); +} + +TEST_F(NavigationControllerTest, LinkClick) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + test_rvh()->SendNavigate(1, url2); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // Should not have produced a new session history entry. + EXPECT_EQ(controller.GetEntryCount(), 2); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); +} + +TEST_F(NavigationControllerTest, InPage) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Main page. + const GURL url1("http://foo"); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // First navigation. + const GURL url2("http://foo#a"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 1; + params.url = url2; + params.transition = content::PAGE_TRANSITION_LINK; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + // This should generate a new entry. + content::LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_TRUE(details.is_in_page); + EXPECT_FALSE(details.did_replace_entry); + EXPECT_EQ(2, controller.GetEntryCount()); + + // Go back one. + ViewHostMsg_FrameNavigate_Params back_params(params); + controller.GoBack(); + back_params.url = url1; + back_params.page_id = 0; + EXPECT_TRUE(controller.RendererDidNavigate(back_params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + // is_in_page is false in that case but should be true. + // See comment in AreURLsInPageNavigation() in navigation_controller.cc + // EXPECT_TRUE(details.is_in_page); + EXPECT_EQ(2, controller.GetEntryCount()); + EXPECT_EQ(0, controller.GetCurrentEntryIndex()); + EXPECT_EQ(back_params.url, controller.GetActiveEntry()->GetURL()); + + // Go forward + ViewHostMsg_FrameNavigate_Params forward_params(params); + controller.GoForward(); + forward_params.url = url2; + forward_params.page_id = 1; + EXPECT_TRUE(controller.RendererDidNavigate(forward_params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_TRUE(details.is_in_page); + EXPECT_EQ(2, controller.GetEntryCount()); + EXPECT_EQ(1, controller.GetCurrentEntryIndex()); + EXPECT_EQ(forward_params.url, + controller.GetActiveEntry()->GetURL()); + + // Now go back and forward again. This is to work around a bug where we would + // compare the incoming URL with the last committed entry rather than the + // one identified by an existing page ID. This would result in the second URL + // losing the reference fragment when you navigate away from it and then back. + controller.GoBack(); + EXPECT_TRUE(controller.RendererDidNavigate(back_params, &details)); + controller.GoForward(); + EXPECT_TRUE(controller.RendererDidNavigate(forward_params, &details)); + EXPECT_EQ(forward_params.url, + controller.GetActiveEntry()->GetURL()); + + // Finally, navigate to an unrelated URL to make sure in_page is not sticky. + const GURL url3("http://bar"); + params.page_id = 2; + params.url = url3; + notifications.Reset(); + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_FALSE(details.is_in_page); +} + +TEST_F(NavigationControllerTest, InPage_Replace) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Main page. + const GURL url1("http://foo"); + test_rvh()->SendNavigate(0, url1); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + + // First navigation. + const GURL url2("http://foo#a"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; // Same page_id + params.url = url2; + params.transition = content::PAGE_TRANSITION_LINK; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url2)); + + // This should NOT generate a new entry, nor prune the list. + content::LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_TRUE(details.is_in_page); + EXPECT_TRUE(details.did_replace_entry); + EXPECT_EQ(1, controller.GetEntryCount()); +} + +// Tests for http://crbug.com/40395 +// Simulates this: +// <script> +// window.location.replace("#a"); +// window.location='http://foo3/'; +// </script> +TEST_F(NavigationControllerTest, ClientRedirectAfterInPageNavigation) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + // Load an initial page. + { + const GURL url("http://foo/"); + test_rvh()->SendNavigate(0, url); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + } + + // Navigate to a new page. + { + const GURL url("http://foo2/"); + test_rvh()->SendNavigate(1, url); + controller.DocumentLoadedInFrame(); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + } + + // Navigate within the page. + { + const GURL url("http://foo2/#a"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 1; // Same page_id + params.url = url; + params.transition = content::PAGE_TRANSITION_LINK; + params.redirects.push_back(url); + params.should_update_history = true; + params.gesture = NavigationGestureUnknown; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url)); + + // This should NOT generate a new entry, nor prune the list. + content::LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_TRUE(details.is_in_page); + EXPECT_TRUE(details.did_replace_entry); + EXPECT_EQ(2, controller.GetEntryCount()); + } + + // Perform a client redirect to a new page. + { + const GURL url("http://foo3/"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 2; // New page_id + params.url = url; + params.transition = content::PAGE_TRANSITION_CLIENT_REDIRECT; + params.redirects.push_back(GURL("http://foo2/#a")); + params.redirects.push_back(url); + params.should_update_history = true; + params.gesture = NavigationGestureUnknown; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url)); + + // This SHOULD generate a new entry. + content::LoadCommittedDetails details; + EXPECT_TRUE(controller.RendererDidNavigate(params, &details)); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_FALSE(details.is_in_page); + EXPECT_EQ(3, controller.GetEntryCount()); + } + + // Verify that BACK brings us back to http://foo2/. + { + const GURL url("http://foo2/"); + controller.GoBack(); + test_rvh()->SendNavigate(1, url); + EXPECT_TRUE(notifications.Check1AndReset( + content::NOTIFICATION_NAV_ENTRY_COMMITTED)); + EXPECT_EQ(url, controller.GetActiveEntry()->GetURL()); + } +} + +// NotificationObserver implementation used in verifying we've received the +// content::NOTIFICATION_NAV_LIST_PRUNED method. +class PrunedListener : public content::NotificationObserver { + public: + explicit PrunedListener(NavigationControllerImpl* controller) + : notification_count_(0) { + registrar_.Add(this, content::NOTIFICATION_NAV_LIST_PRUNED, + content::Source<NavigationController>(controller)); + } + + virtual void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + if (type == content::NOTIFICATION_NAV_LIST_PRUNED) { + notification_count_++; + details_ = *(content::Details<content::PrunedDetails>(details).ptr()); + } + } + + // Number of times NAV_LIST_PRUNED has been observed. + int notification_count_; + + // Details from the last NAV_LIST_PRUNED. + content::PrunedDetails details_; + + private: + content::NotificationRegistrar registrar_; + + DISALLOW_COPY_AND_ASSIGN(PrunedListener); +}; + +// Tests that we limit the number of navigation entries created correctly. +TEST_F(NavigationControllerTest, EnforceMaxNavigationCount) { + NavigationControllerImpl& controller = controller_impl(); + size_t original_count = NavigationControllerImpl::max_entry_count(); + const int kMaxEntryCount = 5; + + NavigationControllerImpl::set_max_entry_count_for_testing(kMaxEntryCount); + + int url_index; + // Load up to the max count, all entries should be there. + for (url_index = 0; url_index < kMaxEntryCount; url_index++) { + GURL url(StringPrintf("http://www.a.com/%d", url_index)); + controller.LoadURL( + url, content::Referrer(), content::PAGE_TRANSITION_TYPED, + std::string()); + test_rvh()->SendNavigate(url_index, url); + } + + EXPECT_EQ(controller.GetEntryCount(), kMaxEntryCount); + + // Created a PrunedListener to observe prune notifications. + PrunedListener listener(&controller); + + // Navigate some more. + GURL url(StringPrintf("http://www.a.com/%d", url_index)); + controller.LoadURL( + url, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(url_index, url); + url_index++; + + // We should have got a pruned navigation. + EXPECT_EQ(1, listener.notification_count_); + EXPECT_TRUE(listener.details_.from_front); + EXPECT_EQ(1, listener.details_.count); + + // We expect http://www.a.com/0 to be gone. + EXPECT_EQ(controller.GetEntryCount(), kMaxEntryCount); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), + GURL("http:////www.a.com/1")); + + // More navigations. + for (int i = 0; i < 3; i++) { + url = GURL(StringPrintf("http:////www.a.com/%d", url_index)); + controller.LoadURL( + url, content::Referrer(), content::PAGE_TRANSITION_TYPED, + std::string()); + test_rvh()->SendNavigate(url_index, url); + url_index++; + } + EXPECT_EQ(controller.GetEntryCount(), kMaxEntryCount); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), + GURL("http:////www.a.com/4")); + + NavigationControllerImpl::set_max_entry_count_for_testing(original_count); +} + +// Tests that we can do a restore and navigate to the restored entries and +// everything is updated properly. This can be tricky since there is no +// SiteInstance for the entries created initially. +TEST_F(NavigationControllerTest, RestoreNavigate) { + // Create a NavigationController with a restored set of tabs. + GURL url("http://foo"); + std::vector<NavigationEntry*> entries; + NavigationEntry* entry = NavigationControllerImpl::CreateNavigationEntry( + url, content::Referrer(), content::PAGE_TRANSITION_RELOAD, false, + std::string(), browser_context()); + entry->SetPageID(0); + entry->SetTitle(ASCIIToUTF16("Title")); + entry->SetContentState("state"); + entries.push_back(entry); + TabContents our_contents( + browser_context(), NULL, MSG_ROUTING_NONE, NULL, NULL); + NavigationControllerImpl& our_controller = our_contents.GetControllerImpl(); + our_controller.Restore(0, true, &entries); + ASSERT_EQ(0u, entries.size()); + + // Before navigating to the restored entry, it should have a restore_type + // and no SiteInstance. + EXPECT_EQ(NavigationEntryImpl::RESTORE_LAST_SESSION, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->restore_type()); + EXPECT_FALSE(NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->site_instance()); + + // After navigating, we should have one entry, and it should be "pending". + // It should now have a SiteInstance and no restore_type. + our_controller.GoToIndex(0); + EXPECT_EQ(1, our_controller.GetEntryCount()); + EXPECT_EQ(our_controller.GetEntryAtIndex(0), + our_controller.GetPendingEntry()); + EXPECT_EQ(0, our_controller.GetEntryAtIndex(0)->GetPageID()); + EXPECT_EQ(NavigationEntryImpl::RESTORE_NONE, + NavigationEntryImpl::FromNavigationEntry + (our_controller.GetEntryAtIndex(0))->restore_type()); + EXPECT_TRUE(NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->site_instance()); + + // Say we navigated to that entry. + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url; + params.transition = content::PAGE_TRANSITION_LINK; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url)); + content::LoadCommittedDetails details; + our_controller.RendererDidNavigate(params, &details); + + // There should be no longer any pending entry and one committed one. This + // means that we were able to locate the entry, assign its site instance, and + // commit it properly. + EXPECT_EQ(1, our_controller.GetEntryCount()); + EXPECT_EQ(0, our_controller.GetLastCommittedEntryIndex()); + EXPECT_FALSE(our_controller.GetPendingEntry()); + EXPECT_EQ(url, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetLastCommittedEntry())->site_instance()-> + GetSite()); + EXPECT_EQ(NavigationEntryImpl::RESTORE_NONE, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->restore_type()); +} + +// Tests that we can still navigate to a restored entry after a different +// navigation fails and clears the pending entry. http://crbug.com/90085 +TEST_F(NavigationControllerTest, RestoreNavigateAfterFailure) { + // Create a NavigationController with a restored set of tabs. + GURL url("http://foo"); + std::vector<NavigationEntry*> entries; + NavigationEntry* entry = NavigationControllerImpl::CreateNavigationEntry( + url, content::Referrer(), content::PAGE_TRANSITION_RELOAD, false, + std::string(), browser_context()); + entry->SetPageID(0); + entry->SetTitle(ASCIIToUTF16("Title")); + entry->SetContentState("state"); + entries.push_back(entry); + TabContents our_contents( + browser_context(), NULL, MSG_ROUTING_NONE, NULL, NULL); + NavigationControllerImpl& our_controller = our_contents.GetControllerImpl(); + our_controller.Restore(0, true, &entries); + ASSERT_EQ(0u, entries.size()); + + // Before navigating to the restored entry, it should have a restore_type + // and no SiteInstance. + EXPECT_EQ(NavigationEntryImpl::RESTORE_LAST_SESSION, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->restore_type()); + EXPECT_FALSE(NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->site_instance()); + + // After navigating, we should have one entry, and it should be "pending". + // It should now have a SiteInstance and no restore_type. + our_controller.GoToIndex(0); + EXPECT_EQ(1, our_controller.GetEntryCount()); + EXPECT_EQ(our_controller.GetEntryAtIndex(0), + our_controller.GetPendingEntry()); + EXPECT_EQ(0, our_controller.GetEntryAtIndex(0)->GetPageID()); + EXPECT_EQ(NavigationEntryImpl::RESTORE_NONE, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->restore_type()); + EXPECT_TRUE(NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->site_instance()); + + // This pending navigation may have caused a different navigation to fail, + // which causes the pending entry to be cleared. + TestRenderViewHost* rvh = + static_cast<TestRenderViewHost*>(our_contents.GetRenderViewHost()); + ViewHostMsg_DidFailProvisionalLoadWithError_Params fail_load_params; + fail_load_params.frame_id = 1; + fail_load_params.is_main_frame = true; + fail_load_params.error_code = net::ERR_ABORTED; + fail_load_params.error_description = string16(); + fail_load_params.url = url; + fail_load_params.showing_repost_interstitial = false; + rvh->OnMessageReceived( + ViewHostMsg_DidFailProvisionalLoadWithError(0, // routing_id + fail_load_params)); + + // Now the pending restored entry commits. + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = url; + params.transition = content::PAGE_TRANSITION_LINK; + params.should_update_history = false; + params.gesture = NavigationGestureUser; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url)); + content::LoadCommittedDetails details; + our_controller.RendererDidNavigate(params, &details); + + // There should be no pending entry and one committed one. + EXPECT_EQ(1, our_controller.GetEntryCount()); + EXPECT_EQ(0, our_controller.GetLastCommittedEntryIndex()); + EXPECT_FALSE(our_controller.GetPendingEntry()); + EXPECT_EQ(url, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetLastCommittedEntry())->site_instance()-> + GetSite()); + EXPECT_EQ(NavigationEntryImpl::RESTORE_NONE, + NavigationEntryImpl::FromNavigationEntry( + our_controller.GetEntryAtIndex(0))->restore_type()); +} + +// Make sure that the page type and stuff is correct after an interstitial. +TEST_F(NavigationControllerTest, Interstitial) { + NavigationControllerImpl& controller = controller_impl(); + // First navigate somewhere normal. + const GURL url1("http://foo"); + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url1); + + // Now navigate somewhere with an interstitial. + const GURL url2("http://bar"); + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + NavigationEntryImpl::FromNavigationEntry(controller.GetPendingEntry())-> + set_page_type(content::PAGE_TYPE_INTERSTITIAL); + + // At this point the interstitial will be displayed and the load will still + // be pending. If the user continues, the load will commit. + test_rvh()->SendNavigate(1, url2); + + // The page should be a normal page again. + EXPECT_EQ(url2, controller.GetLastCommittedEntry()->GetURL()); + EXPECT_EQ(content::PAGE_TYPE_NORMAL, + controller.GetLastCommittedEntry()->GetPageType()); +} + +TEST_F(NavigationControllerTest, RemoveEntry) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + const GURL url4("http://foo/4"); + const GURL url5("http://foo/5"); + const GURL pending_url("http://foo/pending"); + const GURL default_url("http://foo/default"); + + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url1); + controller.LoadURL( + url2, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, url2); + controller.LoadURL( + url3, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(2, url3); + controller.LoadURL( + url4, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(3, url4); + controller.LoadURL( + url5, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(4, url5); + + // Try to remove the last entry. Will fail because it is the current entry. + controller.RemoveEntryAtIndex(controller.GetEntryCount() - 1); + EXPECT_EQ(5, controller.GetEntryCount()); + EXPECT_EQ(4, controller.GetLastCommittedEntryIndex()); + + // Go back and remove the last entry. + controller.GoBack(); + test_rvh()->SendNavigate(3, url4); + controller.RemoveEntryAtIndex(controller.GetEntryCount() - 1); + EXPECT_EQ(4, controller.GetEntryCount()); + EXPECT_EQ(3, controller.GetLastCommittedEntryIndex()); + EXPECT_FALSE(controller.GetPendingEntry()); + + // Remove an entry which is not the last committed one. + controller.RemoveEntryAtIndex(0); + EXPECT_EQ(3, controller.GetEntryCount()); + EXPECT_EQ(2, controller.GetLastCommittedEntryIndex()); + EXPECT_FALSE(controller.GetPendingEntry()); + + // Remove the 2 remaining entries. + controller.RemoveEntryAtIndex(1); + controller.RemoveEntryAtIndex(0); + + // This should leave us with only the last committed entry. + EXPECT_EQ(1, controller.GetEntryCount()); + EXPECT_EQ(0, controller.GetLastCommittedEntryIndex()); +} + +// Tests the transient entry, making sure it goes away with all navigations. +TEST_F(NavigationControllerTest, TransientEntry) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url0("http://foo/0"); + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + const GURL url4("http://foo/4"); + const GURL transient_url("http://foo/transient"); + + controller.LoadURL( + url0, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url0); + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, url1); + + notifications.Reset(); + + // Adding a transient with no pending entry. + NavigationEntryImpl* transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + + // We should not have received any notifications. + EXPECT_EQ(0U, notifications.size()); + + // Check our state. + EXPECT_EQ(transient_url, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(controller.GetEntryCount(), 3); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 1); + EXPECT_EQ(controller.GetPendingEntryIndex(), -1); + EXPECT_TRUE(controller.GetLastCommittedEntry()); + EXPECT_FALSE(controller.GetPendingEntry()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + EXPECT_EQ(contents()->GetMaxPageID(), 1); + + // Navigate. + controller.LoadURL( + url2, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(2, url2); + + // We should have navigated, transient entry should be gone. + EXPECT_EQ(url2, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(controller.GetEntryCount(), 3); + + // Add a transient again, then navigate with no pending entry this time. + transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetActiveEntry()->GetURL()); + test_rvh()->SendNavigate(3, url3); + // Transient entry should be gone. + EXPECT_EQ(url3, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(controller.GetEntryCount(), 4); + + // Initiate a navigation, add a transient then commit navigation. + controller.LoadURL( + url4, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetActiveEntry()->GetURL()); + test_rvh()->SendNavigate(4, url4); + EXPECT_EQ(url4, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(controller.GetEntryCount(), 5); + + // Add a transient and go back. This should simply remove the transient. + transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetActiveEntry()->GetURL()); + EXPECT_TRUE(controller.CanGoBack()); + EXPECT_FALSE(controller.CanGoForward()); + controller.GoBack(); + // Transient entry should be gone. + EXPECT_EQ(url4, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(controller.GetEntryCount(), 5); + test_rvh()->SendNavigate(3, url3); + + // Add a transient and go to an entry before the current one. + transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetActiveEntry()->GetURL()); + controller.GoToIndex(1); + // The navigation should have been initiated, transient entry should be gone. + EXPECT_EQ(url1, controller.GetActiveEntry()->GetURL()); + // Visible entry does not update for history navigations until commit. + EXPECT_EQ(url3, controller.GetVisibleEntry()->GetURL()); + test_rvh()->SendNavigate(1, url1); + EXPECT_EQ(url1, controller.GetVisibleEntry()->GetURL()); + + // Add a transient and go to an entry after the current one. + transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetActiveEntry()->GetURL()); + controller.GoToIndex(3); + // The navigation should have been initiated, transient entry should be gone. + // Because of the transient entry that is removed, going to index 3 makes us + // land on url2 (which is visible after the commit). + EXPECT_EQ(url2, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(url1, controller.GetVisibleEntry()->GetURL()); + test_rvh()->SendNavigate(2, url2); + EXPECT_EQ(url2, controller.GetVisibleEntry()->GetURL()); + + // Add a transient and go forward. + transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + EXPECT_EQ(transient_url, controller.GetActiveEntry()->GetURL()); + EXPECT_TRUE(controller.CanGoForward()); + controller.GoForward(); + // We should have navigated, transient entry should be gone. + EXPECT_EQ(url3, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(url2, controller.GetVisibleEntry()->GetURL()); + test_rvh()->SendNavigate(3, url3); + EXPECT_EQ(url3, controller.GetVisibleEntry()->GetURL()); + + // Ensure the URLS are correct. + EXPECT_EQ(controller.GetEntryCount(), 5); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), url0); + EXPECT_EQ(controller.GetEntryAtIndex(1)->GetURL(), url1); + EXPECT_EQ(controller.GetEntryAtIndex(2)->GetURL(), url2); + EXPECT_EQ(controller.GetEntryAtIndex(3)->GetURL(), url3); + EXPECT_EQ(controller.GetEntryAtIndex(4)->GetURL(), url4); +} + +// Tests that the URLs for renderer-initiated navigations are not displayed to +// the user until the navigation commits, to prevent URL spoof attacks. +// See http://crbug.com/99016. +TEST_F(NavigationControllerTest, DontShowRendererURLUntilCommit) { + NavigationControllerImpl& controller = controller_impl(); + TestNotificationTracker notifications; + RegisterForAllNavNotifications(¬ifications, &controller); + + const GURL url0("http://foo/0"); + const GURL url1("http://foo/1"); + + // For typed navigations (browser-initiated), both active and visible entries + // should update before commit. + controller.LoadURL(url0, content::Referrer(), + content::PAGE_TRANSITION_TYPED, std::string()); + EXPECT_EQ(url0, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(url0, controller.GetVisibleEntry()->GetURL()); + test_rvh()->SendNavigate(0, url0); + + // For link clicks (renderer-initiated navigations), the active entry should + // update before commit but the visible should not. + controller.LoadURLFromRenderer(url1, content::Referrer(), + content::PAGE_TRANSITION_LINK, + std::string()); + EXPECT_EQ(url1, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(url0, controller.GetVisibleEntry()->GetURL()); + EXPECT_TRUE( + NavigationEntryImpl::FromNavigationEntry(controller.GetPendingEntry())-> + is_renderer_initiated()); + + // After commit, both should be updated, and we should no longer treat the + // entry as renderer-initiated. + test_rvh()->SendNavigate(1, url1); + EXPECT_EQ(url1, controller.GetActiveEntry()->GetURL()); + EXPECT_EQ(url1, controller.GetVisibleEntry()->GetURL()); + EXPECT_FALSE( + NavigationEntryImpl::FromNavigationEntry( + controller.GetLastCommittedEntry())->is_renderer_initiated()); + + notifications.Reset(); +} + +// Tests that IsInPageNavigation returns appropriate results. Prevents +// regression for bug 1126349. +TEST_F(NavigationControllerTest, IsInPageNavigation) { + NavigationControllerImpl& controller = controller_impl(); + // Navigate to URL with no refs. + const GURL url("http://www.google.com/home.html"); + test_rvh()->SendNavigate(0, url); + + // Reloading the page is not an in-page navigation. + EXPECT_FALSE(controller.IsURLInPageNavigation(url)); + const GURL other_url("http://www.google.com/add.html"); + EXPECT_FALSE(controller.IsURLInPageNavigation(other_url)); + const GURL url_with_ref("http://www.google.com/home.html#my_ref"); + EXPECT_TRUE(controller.IsURLInPageNavigation(url_with_ref)); + + // Navigate to URL with refs. + test_rvh()->SendNavigate(1, url_with_ref); + + // Reloading the page is not an in-page navigation. + EXPECT_FALSE(controller.IsURLInPageNavigation(url_with_ref)); + EXPECT_FALSE(controller.IsURLInPageNavigation(url)); + EXPECT_FALSE(controller.IsURLInPageNavigation(other_url)); + const GURL other_url_with_ref("http://www.google.com/home.html#my_other_ref"); + EXPECT_TRUE(controller.IsURLInPageNavigation( + other_url_with_ref)); +} + +// Some pages can have subframes with the same base URL (minus the reference) as +// the main page. Even though this is hard, it can happen, and we don't want +// these subframe navigations to affect the toplevel document. They should +// instead be ignored. http://crbug.com/5585 +TEST_F(NavigationControllerTest, SameSubframe) { + NavigationControllerImpl& controller = controller_impl(); + // Navigate the main frame. + const GURL url("http://www.google.com/"); + test_rvh()->SendNavigate(0, url); + + // We should be at the first navigation entry. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); + + // Navigate a subframe that would normally count as in-page. + const GURL subframe("http://www.google.com/#"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = 0; + params.url = subframe; + params.transition = content::PAGE_TRANSITION_AUTO_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(subframe)); + content::LoadCommittedDetails details; + EXPECT_FALSE(controller.RendererDidNavigate(params, &details)); + + // Nothing should have changed. + EXPECT_EQ(controller.GetEntryCount(), 1); + EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 0); +} + +// Make sure that on cloning a tabcontents and going back needs_reload is false. +TEST_F(NavigationControllerTest, CloneAndGoBack) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + + scoped_ptr<WebContents> clone(controller.GetWebContents()->Clone()); + + ASSERT_EQ(2, clone->GetController().GetEntryCount()); + EXPECT_TRUE(clone->GetController().NeedsReload()); + clone->GetController().GoBack(); + // Navigating back should have triggered needs_reload_ to go false. + EXPECT_FALSE(clone->GetController().NeedsReload()); +} + +// Make sure that cloning a tabcontents doesn't copy interstitials. +TEST_F(NavigationControllerTest, CloneOmitsInterstitials) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + + // Add an interstitial entry. Should be deleted with controller. + NavigationEntryImpl* interstitial_entry = new NavigationEntryImpl(); + interstitial_entry->set_page_type(content::PAGE_TYPE_INTERSTITIAL); + controller.AddTransientEntry(interstitial_entry); + + scoped_ptr<WebContents> clone(controller.GetWebContents()->Clone()); + + ASSERT_EQ(2, clone->GetController().GetEntryCount()); +} + +// Tests a subframe navigation while a toplevel navigation is pending. +// http://crbug.com/43967 +TEST_F(NavigationControllerTest, SubframeWhilePending) { + NavigationControllerImpl& controller = controller_impl(); + // Load the first page. + const GURL url1("http://foo/"); + NavigateAndCommit(url1); + + // Now start a pending load to a totally different page, but don't commit it. + const GURL url2("http://bar/"); + controller.LoadURL( + url2, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + + // Send a subframe update from the first page, as if one had just + // automatically loaded. Auto subframes don't increment the page ID. + const GURL url1_sub("http://foo/subframe"); + ViewHostMsg_FrameNavigate_Params params; + params.page_id = controller.GetLastCommittedEntry()->GetPageID(); + params.url = url1_sub; + params.transition = content::PAGE_TRANSITION_AUTO_SUBFRAME; + params.should_update_history = false; + params.gesture = NavigationGestureAuto; + params.is_post = false; + params.content_state = webkit_glue::CreateHistoryStateForURL(GURL(url1_sub)); + content::LoadCommittedDetails details; + + // This should return false meaning that nothing was actually updated. + EXPECT_FALSE(controller.RendererDidNavigate(params, &details)); + + // The notification should have updated the last committed one, and not + // the pending load. + EXPECT_EQ(url1, controller.GetLastCommittedEntry()->GetURL()); + + // The active entry should be unchanged by the subframe load. + EXPECT_EQ(url2, controller.GetActiveEntry()->GetURL()); +} + +// Tests CopyStateFromAndPrune with 2 urls in source, 1 in dest. +TEST_F(NavigationControllerTest, CopyStateFromAndPrune) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + + // First two entries should have the same SiteInstance. + SiteInstance* instance1 = + GetSiteInstanceFromEntry(controller.GetEntryAtIndex(0)); + SiteInstance* instance2 = + GetSiteInstanceFromEntry(controller.GetEntryAtIndex(1)); + EXPECT_EQ(instance1, instance2); + EXPECT_EQ(0, controller.GetEntryAtIndex(0)->GetPageID()); + EXPECT_EQ(1, controller.GetEntryAtIndex(1)->GetPageID()); + EXPECT_EQ(1, contents()->GetMaxPageIDForSiteInstance(instance1)); + + scoped_ptr<TestWebContents> other_contents( + static_cast<TestWebContents*>(CreateTestWebContents())); + NavigationControllerImpl& other_controller = + other_contents->GetControllerImpl(); + other_contents->NavigateAndCommit(url3); + other_contents->ExpectSetHistoryLengthAndPrune( + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(0)), 2, + other_controller.GetEntryAtIndex(0)->GetPageID()); + other_controller.CopyStateFromAndPrune(&controller); + + // other_controller should now contain the 3 urls: url1, url2 and url3. + + ASSERT_EQ(3, other_controller.GetEntryCount()); + + ASSERT_EQ(2, other_controller.GetCurrentEntryIndex()); + + EXPECT_EQ(url1, other_controller.GetEntryAtIndex(0)->GetURL()); + EXPECT_EQ(url2, other_controller.GetEntryAtIndex(1)->GetURL()); + EXPECT_EQ(url3, other_controller.GetEntryAtIndex(2)->GetURL()); + EXPECT_EQ(0, other_controller.GetEntryAtIndex(0)->GetPageID()); + EXPECT_EQ(1, other_controller.GetEntryAtIndex(1)->GetPageID()); + EXPECT_EQ(0, other_controller.GetEntryAtIndex(2)->GetPageID()); + + // A new SiteInstance should be used for the new tab. + SiteInstance* instance3 = + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(2)); + EXPECT_NE(instance3, instance1); + + // The max page ID map should be copied over and updated with the max page ID + // from the current tab. + EXPECT_EQ(1, other_contents->GetMaxPageIDForSiteInstance(instance1)); + EXPECT_EQ(0, other_contents->GetMaxPageIDForSiteInstance(instance3)); +} + +// Test CopyStateFromAndPrune with 2 urls, the first selected and nothing in +// the target. +TEST_F(NavigationControllerTest, CopyStateFromAndPrune2) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + const GURL url3("http://foo3"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + controller.GoBack(); + + scoped_ptr<TestWebContents> other_contents( + static_cast<TestWebContents*>(CreateTestWebContents())); + NavigationControllerImpl& other_controller = + other_contents->GetControllerImpl(); + other_contents->ExpectSetHistoryLengthAndPrune(NULL, 1, -1); + other_controller.CopyStateFromAndPrune(&controller); + + // other_controller should now contain the 1 url: url1. + + ASSERT_EQ(1, other_controller.GetEntryCount()); + + ASSERT_EQ(0, other_controller.GetCurrentEntryIndex()); + + EXPECT_EQ(url1, other_controller.GetEntryAtIndex(0)->GetURL()); + EXPECT_EQ(0, other_controller.GetEntryAtIndex(0)->GetPageID()); + + // The max page ID map should be copied over and updated with the max page ID + // from the current tab. + SiteInstance* instance1 = + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(0)); + EXPECT_EQ(0, other_contents->GetMaxPageIDForSiteInstance(instance1)); +} + +// Test CopyStateFromAndPrune with 2 urls, the first selected and nothing in +// the target. +TEST_F(NavigationControllerTest, CopyStateFromAndPrune3) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + const GURL url2("http://foo2"); + const GURL url3("http://foo3"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + controller.GoBack(); + + scoped_ptr<TestWebContents> other_contents( + static_cast<TestWebContents*>(CreateTestWebContents())); + NavigationControllerImpl& other_controller = + other_contents->GetControllerImpl(); + other_controller.LoadURL( + url3, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + other_contents->ExpectSetHistoryLengthAndPrune(NULL, 1, -1); + other_controller.CopyStateFromAndPrune(&controller); + + // other_controller should now contain 1 entry for url1, and a pending entry + // for url3. + + ASSERT_EQ(1, other_controller.GetEntryCount()); + + EXPECT_EQ(0, other_controller.GetCurrentEntryIndex()); + + EXPECT_EQ(url1, other_controller.GetEntryAtIndex(0)->GetURL()); + + // And there should be a pending entry for url3. + ASSERT_TRUE(other_controller.GetPendingEntry()); + + EXPECT_EQ(url3, other_controller.GetPendingEntry()->GetURL()); + + // The max page ID map should be copied over and updated with the max page ID + // from the current tab. + SiteInstance* instance1 = + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(0)); + EXPECT_EQ(0, other_contents->GetMaxPageIDForSiteInstance(instance1)); +} + +// Tests CopyStateFromAndPrune with 3 urls in source, 1 in dest, +// when the max entry count is 3. We should prune one entry. +TEST_F(NavigationControllerTest, CopyStateFromAndPruneMaxEntries) { + NavigationControllerImpl& controller = controller_impl(); + size_t original_count = NavigationControllerImpl::max_entry_count(); + const int kMaxEntryCount = 3; + + NavigationControllerImpl::set_max_entry_count_for_testing(kMaxEntryCount); + + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + const GURL url4("http://foo/4"); + + // Create a PrunedListener to observe prune notifications. + PrunedListener listener(&controller); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + NavigateAndCommit(url3); + + scoped_ptr<TestWebContents> other_contents( + static_cast<TestWebContents*>(CreateTestWebContents())); + NavigationControllerImpl& other_controller = + other_contents->GetControllerImpl(); + other_contents->NavigateAndCommit(url4); + other_contents->ExpectSetHistoryLengthAndPrune( + GetSiteInstanceFromEntry(other_controller.GetEntryAtIndex(0)), 2, + other_controller.GetEntryAtIndex(0)->GetPageID()); + other_controller.CopyStateFromAndPrune(&controller); + + // We should have received a pruned notification. + EXPECT_EQ(1, listener.notification_count_); + EXPECT_TRUE(listener.details_.from_front); + EXPECT_EQ(1, listener.details_.count); + + // other_controller should now contain only 3 urls: url2, url3 and url4. + + ASSERT_EQ(3, other_controller.GetEntryCount()); + + ASSERT_EQ(2, other_controller.GetCurrentEntryIndex()); + + EXPECT_EQ(url2, other_controller.GetEntryAtIndex(0)->GetURL()); + EXPECT_EQ(url3, other_controller.GetEntryAtIndex(1)->GetURL()); + EXPECT_EQ(url4, other_controller.GetEntryAtIndex(2)->GetURL()); + EXPECT_EQ(1, other_controller.GetEntryAtIndex(0)->GetPageID()); + EXPECT_EQ(2, other_controller.GetEntryAtIndex(1)->GetPageID()); + EXPECT_EQ(0, other_controller.GetEntryAtIndex(2)->GetPageID()); + + NavigationControllerImpl::set_max_entry_count_for_testing(original_count); +} + +// Tests that navigations initiated from the page (with the history object) +// work as expected without navigation entries. +TEST_F(NavigationControllerTest, HistoryNavigate) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + NavigateAndCommit(url3); + controller.GoBack(); + contents()->CommitPendingNavigation(); + + // Simulate the page calling history.back(), it should not create a pending + // entry. + contents()->OnGoToEntryAtOffset(-1); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + // The actual cross-navigation is suspended until the current RVH tells us + // it unloaded, simulate that. + contents()->ProceedWithCrossSiteNavigation(); + // Also make sure we told the page to navigate. + const IPC::Message* message = + process()->sink().GetFirstMessageMatching(ViewMsg_Navigate::ID); + ASSERT_TRUE(message != NULL); + Tuple1<ViewMsg_Navigate_Params> nav_params; + ViewMsg_Navigate::Read(message, &nav_params); + EXPECT_EQ(url1, nav_params.a.url); + process()->sink().ClearMessages(); + + // Now test history.forward() + contents()->OnGoToEntryAtOffset(1); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + // The actual cross-navigation is suspended until the current RVH tells us + // it unloaded, simulate that. + contents()->ProceedWithCrossSiteNavigation(); + message = process()->sink().GetFirstMessageMatching(ViewMsg_Navigate::ID); + ASSERT_TRUE(message != NULL); + ViewMsg_Navigate::Read(message, &nav_params); + EXPECT_EQ(url3, nav_params.a.url); + process()->sink().ClearMessages(); + + // Make sure an extravagant history.go() doesn't break. + contents()->OnGoToEntryAtOffset(120); // Out of bounds. + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + message = process()->sink().GetFirstMessageMatching(ViewMsg_Navigate::ID); + EXPECT_TRUE(message == NULL); +} + +// Test call to PruneAllButActive for the only entry. +TEST_F(NavigationControllerTest, PruneAllButActiveForSingle) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo1"); + NavigateAndCommit(url1); + controller.PruneAllButActive(); + + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), url1); +} + +// Test call to PruneAllButActive for last entry. +TEST_F(NavigationControllerTest, PruneAllButActiveForLast) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + NavigateAndCommit(url3); + controller.GoBack(); + controller.GoBack(); + contents()->CommitPendingNavigation(); + + controller.PruneAllButActive(); + + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), url1); +} + +// Test call to PruneAllButActive for intermediate entry. +TEST_F(NavigationControllerTest, PruneAllButActiveForIntermediate) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + NavigateAndCommit(url3); + controller.GoBack(); + contents()->CommitPendingNavigation(); + + controller.PruneAllButActive(); + + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(controller.GetEntryAtIndex(0)->GetURL(), url2); +} + +// Test call to PruneAllButActive for intermediate entry. +TEST_F(NavigationControllerTest, PruneAllButActiveForPending) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url1("http://foo/1"); + const GURL url2("http://foo/2"); + const GURL url3("http://foo/3"); + + NavigateAndCommit(url1); + NavigateAndCommit(url2); + NavigateAndCommit(url3); + controller.GoBack(); + + controller.PruneAllButActive(); + + EXPECT_EQ(0, controller.GetPendingEntryIndex()); +} + +// Test call to PruneAllButActive for transient entry. +TEST_F(NavigationControllerTest, PruneAllButActiveForTransient) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url0("http://foo/0"); + const GURL url1("http://foo/1"); + const GURL transient_url("http://foo/transient"); + + controller.LoadURL( + url0, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(0, url0); + controller.LoadURL( + url1, content::Referrer(), content::PAGE_TRANSITION_TYPED, std::string()); + test_rvh()->SendNavigate(1, url1); + + // Adding a transient with no pending entry. + NavigationEntryImpl* transient_entry = new NavigationEntryImpl; + transient_entry->SetURL(transient_url); + controller.AddTransientEntry(transient_entry); + + controller.PruneAllButActive(); + + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); + EXPECT_EQ(controller.GetTransientEntry()->GetURL(), transient_url); +} + +// Test to ensure that when we do a history navigation back to the current +// committed page (e.g., going forward to a slow-loading page, then pressing +// the back button), we just stop the navigation to prevent the throbber from +// running continuously. Otherwise, the RenderViewHost forces the throbber to +// start, but WebKit essentially ignores the navigation and never sends a +// message to stop the throbber. +TEST_F(NavigationControllerTest, StopOnHistoryNavigationToCurrentPage) { + NavigationControllerImpl& controller = controller_impl(); + const GURL url0("http://foo/0"); + const GURL url1("http://foo/1"); + + NavigateAndCommit(url0); + NavigateAndCommit(url1); + + // Go back to the original page, then forward to the slow page, then back + controller.GoBack(); + contents()->CommitPendingNavigation(); + + controller.GoForward(); + EXPECT_EQ(1, controller.GetPendingEntryIndex()); + + controller.GoBack(); + EXPECT_EQ(-1, controller.GetPendingEntryIndex()); +} + +/* TODO(brettw) These test pass on my local machine but fail on the XP buildbot + (but not Vista) cleaning up the directory after they run. + This should be fixed. + +// NavigationControllerHistoryTest --------------------------------------------- + +class NavigationControllerHistoryTest : public NavigationControllerTest { + public: + NavigationControllerHistoryTest() + : url0("http://foo1"), + url1("http://foo1"), + url2("http://foo1"), + profile_manager_(NULL) { + } + + virtual ~NavigationControllerHistoryTest() { + // Prevent our base class from deleting the profile since profile's + // lifetime is managed by profile_manager_. + STLDeleteElements(&windows_); + } + + // testing::Test overrides. + virtual void SetUp() { + NavigationControllerTest::SetUp(); + + // Force the session service to be created. + SessionService* service = new SessionService(profile()); + SessionServiceFactory::SetForTestProfile(profile(), service); + service->SetWindowType(window_id, Browser::TYPE_TABBED); + service->SetWindowBounds(window_id, gfx::Rect(0, 1, 2, 3), false); + service->SetTabIndexInWindow(window_id, + controller.session_id(), 0); + controller.SetWindowID(window_id); + + session_helper_.set_service(service); + } + + virtual void TearDown() { + // Release profile's reference to the session service. Otherwise the file + // will still be open and we won't be able to delete the directory below. + session_helper_.ReleaseService(); // profile owns this + SessionServiceFactory::SetForTestProfile(profile(), NULL); + + // Make sure we wait for history to shut down before continuing. The task + // we add will cause our message loop to quit once it is destroyed. + HistoryService* history = + profile()->GetHistoryService(Profile::IMPLICIT_ACCESS); + if (history) { + history->SetOnBackendDestroyTask(MessageLoop::QuitClosure()); + MessageLoop::current()->Run(); + } + + // Do normal cleanup before deleting the profile directory below. + NavigationControllerTest::TearDown(); + + ASSERT_TRUE(file_util::Delete(test_dir_, true)); + ASSERT_FALSE(file_util::PathExists(test_dir_)); + } + + // Deletes the current profile manager and creates a new one. Indirectly this + // shuts down the history database and reopens it. + void ReopenDatabase() { + session_helper_.set_service(NULL); + SessionServiceFactory::SetForTestProfile(profile(), NULL); + + SessionService* service = new SessionService(profile()); + SessionServiceFactory::SetForTestProfile(profile(), service); + session_helper_.set_service(service); + } + + void GetLastSession() { + SessionServiceFactory::GetForProfile(profile())->TabClosed( + controller.window_id(), controller.session_id(), false); + + ReopenDatabase(); + Time close_time; + + session_helper_.ReadWindows(&windows_); + } + + CancelableRequestConsumer consumer; + + // URLs for testing. + const GURL url0; + const GURL url1; + const GURL url2; + + std::vector<SessionWindow*> windows_; + + SessionID window_id; + + SessionServiceTestHelper session_helper_; + + private: + ProfileManager* profile_manager_; + FilePath test_dir_; +}; + +// A basic test case. Navigates to a single url, and make sure the history +// db matches. +TEST_F(NavigationControllerHistoryTest, Basic) { + NavigationControllerImpl& controller = controller_impl(); + controller.LoadURL(url0, GURL(), content::PAGE_TRANSITION_LINK); + test_rvh()->SendNavigate(0, url0); + + GetLastSession(); + + session_helper_.AssertSingleWindowWithSingleTab(windows_, 1); + session_helper_.AssertTabEquals(0, 0, 1, *(windows_[0]->tabs[0])); + TabNavigation nav1(0, url0, GURL(), string16(), + webkit_glue::CreateHistoryStateForURL(url0), + content::PAGE_TRANSITION_LINK); + session_helper_.AssertNavigationEquals(nav1, + windows_[0]->tabs[0]->navigations[0]); +} + +// Navigates to three urls, then goes back and make sure the history database +// is in sync. +TEST_F(NavigationControllerHistoryTest, NavigationThenBack) { + NavigationControllerImpl& controller = controller_impl(); + test_rvh()->SendNavigate(0, url0); + test_rvh()->SendNavigate(1, url1); + test_rvh()->SendNavigate(2, url2); + + controller.GoBack(); + test_rvh()->SendNavigate(1, url1); + + GetLastSession(); + + session_helper_.AssertSingleWindowWithSingleTab(windows_, 3); + session_helper_.AssertTabEquals(0, 1, 3, *(windows_[0]->tabs[0])); + + TabNavigation nav(0, url0, GURL(), string16(), + webkit_glue::CreateHistoryStateForURL(url0), + content::PAGE_TRANSITION_LINK); + session_helper_.AssertNavigationEquals(nav, + windows_[0]->tabs[0]->navigations[0]); + nav.set_url(url1); + session_helper_.AssertNavigationEquals(nav, + windows_[0]->tabs[0]->navigations[1]); + nav.set_url(url2); + session_helper_.AssertNavigationEquals(nav, + windows_[0]->tabs[0]->navigations[2]); +} + +// Navigates to three urls, then goes back twice, then loads a new url. +TEST_F(NavigationControllerHistoryTest, NavigationPruning) { + NavigationControllerImpl& controller = controller_impl(); + test_rvh()->SendNavigate(0, url0); + test_rvh()->SendNavigate(1, url1); + test_rvh()->SendNavigate(2, url2); + + controller.GoBack(); + test_rvh()->SendNavigate(1, url1); + + controller.GoBack(); + test_rvh()->SendNavigate(0, url0); + + test_rvh()->SendNavigate(3, url2); + + // Now have url0, and url2. + + GetLastSession(); + + session_helper_.AssertSingleWindowWithSingleTab(windows_, 2); + session_helper_.AssertTabEquals(0, 1, 2, *(windows_[0]->tabs[0])); + + TabNavigation nav(0, url0, GURL(), string16(), + webkit_glue::CreateHistoryStateForURL(url0), + content::PAGE_TRANSITION_LINK); + session_helper_.AssertNavigationEquals(nav, + windows_[0]->tabs[0]->navigations[0]); + nav.set_url(url2); + session_helper_.AssertNavigationEquals(nav, + windows_[0]->tabs[0]->navigations[1]); +} +*/ |