// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stdint.h>

#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/ranges/algorithm.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "content/browser/child_process_security_policy_impl.h"
#include "content/browser/process_lock.h"
#include "content/browser/renderer_host/frame_navigation_entry.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/navigation_controller_impl.h"
#include "content/browser/renderer_host/navigation_type.h"
#include "content/browser/renderer_host/navigation_entry_impl.h"
#include "content/browser/renderer_host/navigation_entry_restore_context_impl.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/renderer_host/renderer_cancellation_throttle.h"
#include "content/browser/site_info.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/web_contents/web_contents_view.h"
#include "content/common/content_navigation_policy.h"
#include "content/common/features.h"
#include "content/common/frame_messages.mojom.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/bindings_policy.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/download_test_observer.h"
#include "content/public/test/navigation_handle_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/url_loader_interceptor.h"
#include "content/shell/browser/shell.h"
#include "content/shell/browser/shell_download_manager_delegate.h"
#include "content/shell/common/shell_switches.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/did_commit_navigation_interceptor.h"
#include "content/test/render_document_feature.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/url_request/url_request_failed_job.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/loader/referrer_utils.h"
#include "third_party/blink/public/common/page_state/page_state_serialization.h"
#include "third_party/blink/public/common/renderer_preferences/renderer_preferences.h"
#include "third_party/blink/public/mojom/frame/frame.mojom-test-utils.h"
#include "third_party/blink/public/mojom/frame/user_activation_update_types.mojom.h"
#include "ui/display/display_util.h"
#include "url/url_constants.h"

namespace content {
namespace {

using testing::ElementsAre;
using testing::IsEmpty;
using InitialNavigationEntryState =
    NavigationEntryImpl::InitialNavigationEntryState;

const char kAddNamedFrameScript[] =
    "var f = document.createElement('iframe');"
    "f.name = 'foo-frame-name';"
    "document.body.appendChild(f);";
const char kRemoveFrameScript[] =
    "var f = document.querySelector('iframe');"
    "f.parentNode.removeChild(f);";

const char kAddEmptyFrameScript[] =
    "let iframe = document.createElement('iframe');"
    "document.body.appendChild(iframe);";

const char kAddFrameWithSrcScript[] =
    "let iframe = document.createElement('iframe');"
    "iframe.src = $1;"
    "document.body.appendChild(iframe);";

}  // namespace

class NavigationControllerBrowserTestBase : public ContentBrowserTest {
 public:
  NavigationControllerBrowserTestBase() {
    feature_list_.InitWithFeaturesAndParameters(
        {{kQueueNavigationsWhileWaitingForCommit, {{"level", "full"}}}}, {});
  }

 protected:
  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    content::SetupCrossSiteRedirector(embedded_test_server());
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    ContentBrowserTest::SetUpCommandLine(command_line);
    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        switches::kExposeInternalsForTesting);
  }

  WebContentsImpl* contents() const {
    return static_cast<WebContentsImpl*>(shell()->web_contents());
  }

  void LoadDataWithBaseURL(const GURL& base_url,
                           const std::string& data,
                           const GURL& history_url,
                           const std::string& title,
                           bool use_load_data_as_string_with_base_url) {
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    TitleWatcher title_watcher(shell()->web_contents(),
                               base::UTF8ToUTF16(title));
    if (use_load_data_as_string_with_base_url) {
#if BUILDFLAG(IS_ANDROID)
      shell()->LoadDataAsStringWithBaseURL(history_url, data, base_url);
#else
      NOTREACHED();
#endif
    } else {
      shell()->LoadDataWithBaseURL(history_url, data, base_url);
    }
    same_tab_observer.Wait();
    std::u16string actual_title = title_watcher.WaitAndGetTitle();
    EXPECT_EQ(title, base::UTF16ToUTF8(actual_title));
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

class NavigationControllerBrowserTest
    : public NavigationControllerBrowserTestBase,
      public ::testing::WithParamInterface<
          std::tuple<std::string /* render_document_level */,
                     bool /* enable_back_forward_cache*/>> {
 public:
  NavigationControllerBrowserTest() {
    InitAndEnableRenderDocumentFeature(&feature_list_for_render_document_,
                                       std::get<0>(GetParam()));
    InitBackForwardCacheFeature(&feature_list_for_back_forward_cache_,
                                std::get<1>(GetParam()));
  }

  // Provides meaningful param names instead of /0, /1, ...
  static std::string DescribeParams(
      const testing::TestParamInfo<ParamType>& info) {
    auto [render_document_level, enable_back_forward_cache] = info.param;
    return base::StringPrintf(
        "%s_%s",
        GetRenderDocumentLevelNameForTestParams(render_document_level).c_str(),
        enable_back_forward_cache ? "BFCacheEnabled" : "BFCacheDisabled");
  }

  RenderFrameHostImpl* current_main_frame_host() {
    return static_cast<RenderFrameHostImpl*>(contents()->GetPrimaryMainFrame());
  }

 protected:
  // TODO(bokan): There's one test whose result depends on whether the
  // FractionalScrollOffsets feature is enabled in Blink's
  // RuntimeEnabledFeatures. Since there's just one, we can determine the
  // status of the feature using script, rather than plumbing this state across
  // the browser/renderer boundary. This can be removed once the feature is
  // shipped to stable and the flag removed. https://crbug.com/414283.
  bool IsFractionalScrollOffsetsEnabled() {
    std::string script =
        "internals.runtimeFlags.fractionalScrollOffsetsEnabled";
    return EvalJs(shell(), script).ExtractBool();
  }

  // Creates a form and submits it to |form_submit_url|. Returns the POST ID of
  // the submitted form POST data.
  int64_t CreateAndSubmitForm(const GURL& form_submit_url) {
    NavigationControllerImpl& controller =
        static_cast<NavigationControllerImpl&>(contents()->GetController());
    TestNavigationObserver form_nav_observer(contents());
    EXPECT_TRUE(ExecJs(contents(),
                       JsReplace(R"(var form = document.createElement('form');
                                 form.method = 'POST';
                                 form.action = $1;
                                 document.body.appendChild(form);
                                 form.submit();)",
                                 form_submit_url)));
    form_nav_observer.Wait();
    EXPECT_EQ(form_submit_url, contents()->GetLastCommittedURL());
    const int64_t form_post_id =
        controller.GetLastCommittedEntry()->GetPostID();
    EXPECT_NE(-1, form_post_id);
    EXPECT_TRUE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ("POST", contents()->GetPrimaryMainFrame()->last_http_method());
    return form_post_id;
  }

  void ReplaceState(FrameTreeNode* node,
                    const std::string& state,
                    GURL url = GURL()) {
    FrameNavigateParamsCapturer capturer(node);
    capturer.set_wait_for_load(false);
    if (url.is_empty()) {
      EXPECT_TRUE(
          ExecJs(node, JsReplace("history.replaceState($1, '')", state)));
    } else {
      EXPECT_TRUE(ExecJs(
          node, JsReplace("history.replaceState($1, '', $2)", state, url)));
    }
    capturer.Wait();
    EXPECT_TRUE(capturer.is_same_document());
  }

 private:
  base::test::ScopedFeatureList feature_list_for_render_document_;
  base::test::ScopedFeatureList feature_list_for_back_forward_cache_;
};

// Base class for tests that need to supply modifications to EmbeddedTestServer
// which are required to be complete before it is started.
class NavigationControllerBrowserTestNoServer
    : public ContentBrowserTest,
      public ::testing::WithParamInterface<
          std::tuple<std::string /* render_document_level */,
                     bool /* enable_back_forward_cache*/>> {
 public:
  NavigationControllerBrowserTestNoServer() {
    InitAndEnableRenderDocumentFeature(&feature_list_for_render_document_,
                                       std::get<0>(GetParam()));
    InitBackForwardCacheFeature(&feature_list_for_back_forward_cache_,
                                std::get<1>(GetParam()));
  }

 protected:
  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    content::SetupCrossSiteRedirector(embedded_test_server());
  }

  WebContentsImpl* contents() const {
    return static_cast<WebContentsImpl*>(shell()->web_contents());
  }

 private:
  base::test::ScopedFeatureList feature_list_for_render_document_;
  base::test::ScopedFeatureList feature_list_for_back_forward_cache_;
};

// Ensure that tests can navigate subframes cross-site in both default mode and
// --site-per-process, but that they only go cross-process in the latter.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, LoadCrossSiteSubframe) {
  // Load a main frame with a subframe.
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(1U, root->child_count());
  ASSERT_NE(nullptr, root->child_at(0));

  // Use NavigateToURLFromRenderer to go cross-site in the subframe.
  GURL foo_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), foo_url));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  // We should only have swapped processes in --site-per-process.
  bool cross_process = root->current_frame_host()->GetProcess() !=
                       root->child_at(0)->current_frame_host()->GetProcess();
  EXPECT_EQ(AreAllSitesIsolatedForTesting(), cross_process);
}

// Expects the referrer URL saved in `entry` is `url`, and that the referrer
// uses the default referrer policy.
void ExpectReferrerWithDefaultPolicy(FrameNavigationEntry* entry,
                                     const GURL& url) {
  EXPECT_EQ(url, entry->referrer().url);
  EXPECT_EQ(blink::ReferrerUtils::MojoReferrerPolicyResolveDefault(
                network::mojom::ReferrerPolicy::kDefault),
            blink::ReferrerUtils::MojoReferrerPolicyResolveDefault(
                entry->referrer().policy));
}

// Same as above, but takes in a NavigationEntry instead.
void ExpectReferrerWithDefaultPolicy(NavigationEntry* entry, const GURL& url) {
  ExpectReferrerWithDefaultPolicy(
      static_cast<NavigationEntryImpl*>(entry)->root_node()->frame_entry.get(),
      url);
}

class LoadDataWithBaseURLWithPossiblyEmptyURLsBrowserTest
    : public NavigationControllerBrowserTestBase,
      public testing::WithParamInterface<
          std::tuple<std::string /* render_document_level */,
                     bool /* use_load_data_as_string_with_base_url */,
                     bool /* base_url_empty */,
                     bool /* history_url_empty */>> {
 public:
  LoadDataWithBaseURLWithPossiblyEmptyURLsBrowserTest() {
    InitAndEnableRenderDocumentFeature(&feature_list_for_render_document_,
                                       std::get<0>(GetParam()));
  }

  // Provides meaningful param names instead of /0, /1, ...
  static std::string DescribeParams(
      const testing::TestParamInfo<ParamType>& info) {
    auto [render_document_level, use_load_data_as_string_with_base_url,
          base_url_empty, history_url_empty] = info.param;
    return base::StringPrintf(
        "%s_%s_%s_%s",
        GetRenderDocumentLevelNameForTestParams(render_document_level).c_str(),
        use_load_data_as_string_with_base_url ? "AsString" : "Normal",
        base_url_empty ? "BaseURLEmpty" : "BaseURLSet",
        history_url_empty ? "HistoryURLEmpty" : "HistoryURLSet");
  }

 protected:
  bool use_load_data_as_string_with_base_url() {
    return std::get<1>(GetParam());
  }
  bool base_url_empty() { return std::get<2>(GetParam()); }
  bool history_url_empty() { return std::get<3>(GetParam()); }

  // Helper struct to combine all URLs involved for LoadDataWithBaseURL
  // navigations.
  struct URLsForLoadDataWithBaseURL {
    GURL supplied_base_url;
    GURL commit_url;
    GURL virtual_url;
    GURL history_url_for_data_url;
    GURL document_url;
  };

  void CheckURLsMatchExpectationsForLoadDataWithBaseURL(
      NavigationEntryImpl* entry,
      RenderFrameHostImpl* rfh,
      URLsForLoadDataWithBaseURL& urls,
      bool is_renderer_initiated_same_document_navigation = false) {
    EXPECT_EQ(urls.document_url, rfh->last_document_url_in_renderer());
    // The URL in the session history entry will use the commit URL.
    EXPECT_EQ(urls.commit_url, entry->GetURL());
    // Regardless of whether the supplied base URL and history URL are actually
    // used in the renderer or not, they will be saved in the entry.
    EXPECT_EQ(urls.supplied_base_url, entry->GetBaseURLForDataURL());
    EXPECT_EQ(urls.history_url_for_data_url, entry->GetHistoryURLForDataURL());
    EXPECT_EQ(urls.virtual_url, entry->GetVirtualURL());
    // We should use `commit_url` instead of `supplied_base_url` as the original
    // url of this navigation entry, because `supplied_base_url` is only used
    // for resolving relative paths in the data, or enforcing same origin policy
    // (not to actually load the initial contents of the page).
    EXPECT_EQ(urls.commit_url, entry->GetOriginalRequestURL());

    // For cross-document or browser-initiated navigations, the redirect chain
    // contains the document URL.
    // TODO(https://crbug.com/1171237): Should we use the commit URL instead?
    if (!is_renderer_initiated_same_document_navigation) {
      EXPECT_EQ(1u, entry->GetRedirectChain().size());
      EXPECT_EQ(urls.document_url, entry->GetRedirectChain()[0]);
    }
  }

  URLsForLoadDataWithBaseURL GetURLsForLoadDataWithBaseURL(
      const GURL& commit_url,
      const GURL& supplied_base_url,
      const GURL& supplied_history_url) {
    URLsForLoadDataWithBaseURL result;
    result.supplied_base_url = supplied_base_url;
    // The "commit URL" will always be set to the URL used for commit (the data
    // URL/header).
    result.commit_url = commit_url;
    // The virtual URL (the URL shown in the address bar) will be the supplied
    // history url (*not* the renderer history URL), unless it's empty, in which
    // case we'll fall back to the entry URL (see
    // NavigationEntry::GetVirtualURL()).
    result.virtual_url = supplied_history_url.is_empty() ? result.commit_url
                                                         : supplied_history_url;
    // The "history URL for data URL" in the NavigationEntry will always be set
    // to the virtual URL, unless the base URL is empty, in which case it
    // returns an empty url(see NavigationEntry::GetHistoryURLForDataURL())
    result.history_url_for_data_url =
        supplied_base_url.is_empty() ? GURL() : result.virtual_url;

    // The "document URL" will be set to the base URL, unless the navigation is
    // not treated as a loadDataWithBaseURL navigation or the base URL is empty,
    // in which case it will use the commit URL.
    result.document_url =
        !supplied_base_url.is_empty() ? supplied_base_url : result.commit_url;
    return result;
  }

 private:
  base::test::ScopedFeatureList feature_list_for_render_document_;
};

// Verifies that the base, history, and data URLs for LoadDataWithBaseURL end up
// in the expected parts of the NavigationEntry in each stage of navigation, and
// that we don't kill the renderer on reload.  See https://crbug.com/522567.
// Having the base/history URLs as empty might affect things like which URLs are
// used in the NavigationEntries.
IN_PROC_BROWSER_TEST_P(LoadDataWithBaseURLWithPossiblyEmptyURLsBrowserTest,
                       LoadDataWithBaseURLThenReload) {
#if !BUILDFLAG(IS_ANDROID)
  // LoadDataAsStringWithBaseURL is only supported on Android.
  if (use_load_data_as_string_with_base_url())
    return;
#endif
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;

  const std::string data_header = "data:text/html;charset=utf-8,";
  const std::string title = "foo";
  const std::string data = base::StringPrintf(
      "<html><head><title>%s</title></head><body>foo</body></html>",
      title.c_str());
  const GURL data_url = GURL(data_header + data);
  // `base_url_for_data_url` in LoadURLParams.
  const GURL supplied_base_url =
      base_url_empty() ? GURL() : GURL("http://baseurl");
  // `history_url_for_data_url` in LoadURLParams.
  const GURL supplied_history_url =
      history_url_empty() ? GURL() : GURL("http://historyurl");
  // `url` in LoadURLParams.
  const GURL commit_url =
      use_load_data_as_string_with_base_url() ? GURL(data_header) : data_url;
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // 1) Load data, but don't commit yet.
  TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
  if (use_load_data_as_string_with_base_url()) {
#if BUILDFLAG(IS_ANDROID)
    shell()->LoadDataAsStringWithBaseURL(supplied_history_url, data,
                                         supplied_base_url);
#else
    NOTREACHED();
#endif
  } else {
    shell()->LoadDataWithBaseURL(supplied_history_url, data, supplied_base_url);
  }

  // Verify the pending NavigationEntry.
  NavigationEntryImpl* pending_entry = controller.GetPendingEntry();
  URLsForLoadDataWithBaseURL urls = GetURLsForLoadDataWithBaseURL(
      commit_url, supplied_base_url, supplied_history_url);

  // The URL of the entry will always be set to the URL used for commit.
  EXPECT_EQ(urls.commit_url, pending_entry->GetURL());
  EXPECT_EQ(urls.supplied_base_url, pending_entry->GetBaseURLForDataURL());
  EXPECT_EQ(urls.virtual_url, pending_entry->GetVirtualURL());
  EXPECT_EQ(urls.history_url_for_data_url,
            pending_entry->GetHistoryURLForDataURL());

  // 2) Let the navigation commit.
  same_tab_observer.Wait();

  // Verify the last committed NavigationEntry has correct HTTP status code
  // and URLs.
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  RenderFrameHostImpl* current_rfh =
      static_cast<RenderFrameHostImpl*>(contents()->GetPrimaryMainFrame());

  {
    SCOPED_TRACE(testing::Message() << " Testing case 2.");
    CheckURLsMatchExpectationsForLoadDataWithBaseURL(entry, current_rfh, urls);
  }

  // data: URL loads always have HTTP status code 200.
  EXPECT_EQ(200, current_rfh->last_http_status_code());

  // Verify that the page is not classified as an error page.
  EXPECT_EQ(PAGE_TYPE_NORMAL, entry->GetPageType());
  EXPECT_FALSE(current_rfh->IsErrorDocument());

  // The original request URL for loadDataWithBaseURL navigations will be the
  // URL used for commit (the data URL/header).
  EXPECT_EQ(entry->GetOriginalRequestURL(), urls.commit_url);

  // No referrer since this is a browser-initiated navigation.
  ExpectReferrerWithDefaultPolicy(entry, GURL());

  // 3) Now reload and make sure the renderer isn't killed.
  ReloadBlockUntilNavigationsComplete(shell(), 1);
  current_rfh =
      static_cast<RenderFrameHostImpl*>(contents()->GetPrimaryMainFrame());
  EXPECT_TRUE(current_rfh->IsRenderFrameLive());

  // Verify the last committed NavigationEntry and all the URLs hasn't changed.
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntryImpl* reload_entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(reload_entry, entry);
  {
    SCOPED_TRACE(testing::Message() << " Testing case 3.");
    CheckURLsMatchExpectationsForLoadDataWithBaseURL(reload_entry, current_rfh,
                                                     urls);
  }

  // 4) Now do a same-URL navigation, where all the URLs are the exact same as
  // before. This will be turned into a reload by
  // ShouldTreatNavigationAsReload() in navigation_controller_impl.cc.
  LoadDataWithBaseURL(supplied_base_url, data, supplied_history_url, title,
                      use_load_data_as_string_with_base_url());

  // Verify the last committed NavigationEntry and all the URLs hasn't changed.
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntryImpl* same_url_entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(reload_entry, same_url_entry);

  {
    SCOPED_TRACE(testing::Message() << " Testing case 4.");
    CheckURLsMatchExpectationsForLoadDataWithBaseURL(
        same_url_entry,
        static_cast<RenderFrameHostImpl*>(contents()->GetPrimaryMainFrame()),
        urls);
  }
}

// Similar to LoadDataWithBaseURLThenRendererInitiatedSameDocument but tests
// back navigation after doing some renderer-initiated same-document
// navigations.
IN_PROC_BROWSER_TEST_P(
    LoadDataWithBaseURLWithPossiblyEmptyURLsBrowserTest,
    HistoryNavigationWhenLoadDataWithBaseURLWithSameDocumentNavigation) {
#if !BUILDFLAG(IS_ANDROID)
  // LoadDataAsStringWithBaseURL is only supported on Android.
  if (use_load_data_as_string_with_base_url())
    return;
#endif
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;
  // Renderer-initiated navigations to data: URL on a main frame will be
  // blocked. If the base URL is empty, the renderer will use the data: URL as
  // the base & document URL, and same-document navigations initiated by the
  // renderer will try to do a main frame navigation to a data: URL, which will
  // be blocked. So, return early instead.
  if (base_url_empty())
    return;

  const std::string data_header = "data:text/html;charset=utf-8,";
  const std::string title = "foo";
  const std::string data = base::StringPrintf(
      "<html><head><title>%s</title></head><body>foo</body></html>",
      title.c_str());
  const GURL data_url = GURL(data_header + data);
  // `base_url_for_data_url` in LoadURLParams.
  const GURL supplied_base_url =
      base_url_empty() ? GURL() : GURL("http://baseurl");
  // `history_url_for_data_url` in LoadURLParams.
  const GURL supplied_history_url =
      history_url_empty() ? GURL() : GURL("http://historyurl");
  // `url` in LoadURLParams.
  const GURL commit_url =
      use_load_data_as_string_with_base_url() ? GURL(data_header) : data_url;
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  LoadDataWithBaseURL(supplied_base_url, data, supplied_history_url, title,
                      use_load_data_as_string_with_base_url());

  URLsForLoadDataWithBaseURL urls = GetURLsForLoadDataWithBaseURL(
      commit_url, supplied_base_url, supplied_history_url);

  CheckURLsMatchExpectationsForLoadDataWithBaseURL(
      controller.GetLastCommittedEntry(),
      static_cast<RenderFrameHostImpl*>(contents()->GetPrimaryMainFrame()),
      urls);

  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  // 2) Do a same-document navigation via pushState. The URLs will stay the
  // same (actually the base/document URL should have changed, but the browser
  // has no way of knowing that).
  {
    FrameNavigateParamsCapturer capturer(root);
    capturer.set_wait_for_load(false);
    EXPECT_TRUE(ExecJs(shell(), "history.pushState({},'foo1', '#bar1')"));
    capturer.Wait();
    EXPECT_TRUE(capturer.is_same_document());

    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    CheckURLsMatchExpectationsForLoadDataWithBaseURL(
        entry,
        static_cast<RenderFrameHostImpl*>(contents()->GetPrimaryMainFrame()),
        urls, true /* is_renderer_initiated_same_document_navigation */);
    scoped_refptr<FrameNavigationEntry> frame_entry =
        entry->root_node()->frame_entry.get();
    if (entry->GetBaseURLForDataURL().is_empty()) {
      // when the base URL is empty, the navigation is treated like a normal
      // data: URL navigation, so the origin is opaque.
      EXPECT_TRUE(frame_entry->committed_origin()->opaque());
    } else {
      // loadDataWithBaseURL() navigations uses the base URL's origin.
      EXPECT_EQ(url::Origin::Create(supplied_base_url),
                frame_entry->committed_origin());
    }
  }

  // 3) Do a same-document navigation via pushState. The URLs will stay the
  // same (actually the base/document URL should have changed, but the browser
  // has no way of knowing that).
  {
    FrameNavigateParamsCapturer capturer(root);
    capturer.set_wait_for_load(false);
    EXPECT_TRUE(ExecJs(shell(), "history.pushState({},'foo2', '#bar2')"));
    capturer.Wait();
    EXPECT_TRUE(capturer.is_same_document());

    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    CheckURLsMatchExpectationsForLoadDataWithBaseURL(
        entry,
        static_cast<RenderFrameHostImpl*>(contents()->GetPrimaryMainFrame()),
        urls, true /* is_renderer_initiated_same_document_navigation */);
    scoped_refptr<FrameNavigationEntry> frame_entry =
        entry->root_node()->frame_entry.get();
    if (entry->GetBaseURLForDataURL().is_empty()) {
      // when the base URL is empty, the navigation is treated like a normal
      // data: URL navigation, so the origin is opaque.
      EXPECT_TRUE(frame_entry->committed_origin()->opaque());
    } else {
      // loadDataWithBaseURL() navigations uses the base URL's origin.
      EXPECT_EQ(url::Origin::Create(supplied_base_url),
                frame_entry->committed_origin());
    }
  }

  // 4) Do a history navigation via GoBack. GoBack needs to be successful.
  {
    FrameNavigateParamsCapturer capturer(root);
    controller.GoBack();
    capturer.Wait();
    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
    scoped_refptr<FrameNavigationEntry> frame_entry =
        entry->root_node()->frame_entry.get();
    if (entry->GetBaseURLForDataURL().is_empty()) {
      // when the base URL is empty, the navigation is treated like a normal
      // data: URL navigation, so the origin is opaque.
      EXPECT_TRUE(frame_entry->committed_origin()->opaque());
    } else {
      // loadDataWithBaseURL() navigations uses the base URL's origin.
      EXPECT_EQ(url::Origin::Create(supplied_base_url),
                frame_entry->committed_origin());
    }
  }
}

// Similar to LoadDataWithBaseURLThenReload but tests renderer-initiated
// same-document navigations after LoadDataWithBaseURL instead.
IN_PROC_BROWSER_TEST_P(LoadDataWithBaseURLWithPossiblyEmptyURLsBrowserTest,
                       LoadDataWithBaseURLThenRendererInitiatedSameDocument) {
#if !BUILDFLAG(IS_ANDROID)
  // LoadDataAsStringWithBaseURL is only supported on Android.
  if (use_load_data_as_string_with_base_url())
    return;
#endif
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;
  // Renderer-initiated navigations to data: URL on a main frame will be
  // blocked. If the base URL is empty, the renderer will use the data: URL as
  // the base & document URL, and same-document navigations initiated by the
  // renderer will try to do a main frame navigation to a data: URL, which will
  // be blocked. So, return early instead.
  if (base_url_empty())
    return;

  const std::string data_header = "data:text/html;charset=utf-8,";
  const std::string title = "foo";
  const std::string data = base::StringPrintf(
      "<html><head><title>%s</title></head><body>foo</body></html>",
      title.c_str());
  const GURL data_url = GURL(data_header + data);
  // `base_url_for_data_url` in LoadURLParams.
  const GURL supplied_base_url =
      base_url_empty() ? GURL() : GURL("http://baseurl");
  // `history_url_for_data_url` in LoadURLParams.
  const GURL supplied_history_url =
      history_url_empty() ? GURL() : GURL("http://historyurl");
  // `url` in LoadURLParams.
  const GURL commit_url =
      use_load_data_as_string_with_base_url() ? GURL(data_header) : data_url;
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  LoadDataWithBaseURL(supplied_base_url, data, supplied_history_url, title,
                      use_load_data_as_string_with_base_url());

  URLsForLoadDataWithBaseURL urls = GetURLsForLoadDataWithBaseURL(
      commit_url, supplied_base_url, supplied_history_url);

  CheckURLsMatchExpectationsForLoadDataWithBaseURL(
      controller.GetLastCommittedEntry(),
      static_cast<RenderFrameHostImpl*>(contents()->GetPrimaryMainFrame()),
      urls);

  // 2) Make a same-document navigation by navigating to the document URL but
  // adding a fragment in the end. No URLs change on the browser side (actually
  // the base/document URL should have changed, but the browser has no way of
  // knowing that).
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  {
    SCOPED_TRACE(testing::Message() << " Testing case 2.");
    FrameNavigateParamsCapturer capturer(root);
    capturer.set_wait_for_load(false);
    GURL document_url_with_fragment = GURL(urls.document_url.spec() + "#foo");
    EXPECT_TRUE(ExecJs(
        shell(), JsReplace("location.href = $1;", document_url_with_fragment)));
    capturer.Wait();
    EXPECT_TRUE(capturer.is_same_document());
    CheckURLsMatchExpectationsForLoadDataWithBaseURL(
        controller.GetLastCommittedEntry(),
        static_cast<RenderFrameHostImpl*>(contents()->GetPrimaryMainFrame()),
        urls, true /* is_renderer_initiated_same_document_navigation */);
  }

  // 3) Do a same-document navigation via pushState. The URLs will stay the
  // same (actually the base/document URL should have changed, but the browser
  // has no way of knowing that).
  {
    SCOPED_TRACE(testing::Message() << " Testing case 3.");
    FrameNavigateParamsCapturer capturer(root);
    capturer.set_wait_for_load(false);
    EXPECT_TRUE(ExecJs(shell(), "history.pushState({},'foo', 'bar')"));
    capturer.Wait();
    EXPECT_TRUE(capturer.is_same_document());

    CheckURLsMatchExpectationsForLoadDataWithBaseURL(
        controller.GetLastCommittedEntry(),
        static_cast<RenderFrameHostImpl*>(contents()->GetPrimaryMainFrame()),
        urls, true /* is_renderer_initiated_same_document_navigation */);
  }
}

// Similar to LoadDataWithBaseURLThenReload but tests browser-initiated
// same-document navigations after LoadDataWithBaseURL instead.
IN_PROC_BROWSER_TEST_P(LoadDataWithBaseURLWithPossiblyEmptyURLsBrowserTest,
                       LoadDataWithBaseURLThenBrowserInitiatedSameDocument) {
#if !BUILDFLAG(IS_ANDROID)
  // LoadDataAsStringWithBaseURL is only supported on Android.
  if (use_load_data_as_string_with_base_url())
    return;
#endif
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;

  const std::string data_header = "data:text/html;charset=utf-8,";
  const std::string title = "foo";
  const std::string data = base::StringPrintf(
      "<html><head><title>%s</title></head><body>foo</body></html>",
      title.c_str());
  const GURL data_url = GURL(data_header + data);
  // `base_url_for_data_url` in LoadURLParams.
  const GURL supplied_base_url =
      base_url_empty() ? GURL()
                       : embedded_test_server()->GetURL("/title1.html");
  // `history_url_for_data_url` in LoadURLParams.
  const GURL supplied_history_url =
      history_url_empty() ? GURL()
                          : embedded_test_server()->GetURL("/title2.html");
  // `url` in LoadURLParams.
  const GURL commit_url =
      use_load_data_as_string_with_base_url() ? GURL(data_header) : data_url;
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // Do a LoadDataWithBaseURL navigation.
  LoadDataWithBaseURL(supplied_base_url, data, supplied_history_url, title,
                      use_load_data_as_string_with_base_url());

  URLsForLoadDataWithBaseURL urls = GetURLsForLoadDataWithBaseURL(
      commit_url, supplied_base_url, supplied_history_url);
  CheckURLsMatchExpectationsForLoadDataWithBaseURL(
      controller.GetLastCommittedEntry(),
      static_cast<RenderFrameHostImpl*>(contents()->GetPrimaryMainFrame()),
      urls);

  // 1) Try to make a same-document navigation from the browser after
  // LoadDataWithBaseURL by navigating to the commit URL + "#foo". This will
  // start out as same-document but might be restarted as cross-document if it
  // doesn't match the document URL. All cases will just end up being a regular
  // data: URL commit (either same-document or cross-document).
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  {
    SCOPED_TRACE(testing::Message() << " Testing case 1. ");

    FrameNavigateParamsCapturer capturer(root);
    capturer.set_wait_for_load(false);
    GURL commit_url_with_fragment = GURL(commit_url.spec() + "#foo");
    NavigationController::LoadURLParams params(commit_url_with_fragment);
    params.transition_type = ui::PageTransitionFromInt(
        ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
    contents()->GetController().LoadURLWithParams(params);
    capturer.Wait();
    EXPECT_EQ(urls.document_url == urls.commit_url,
              capturer.is_same_document());

    // Update the URL expectations for the regular data: URL commit.
    urls.commit_url = commit_url_with_fragment;
    urls.document_url = commit_url_with_fragment;
    urls.history_url_for_data_url = GURL();
    urls.supplied_base_url = GURL();
    // If the navigation is classified as a same document, it might use the
    // previous history URL as the virtual URL, probably unintentionally copied
    // from the previous NavigationEntry.
    urls.virtual_url = (capturer.is_same_document() && !history_url_empty())
                           ? supplied_history_url
                           : commit_url_with_fragment;

    CheckURLsMatchExpectationsForLoadDataWithBaseURL(
        controller.GetLastCommittedEntry(),
        static_cast<RenderFrameHostImpl*>(contents()->GetPrimaryMainFrame()),
        urls);
  }

  // Do another LoadDataWithBaseURL navigation.
  LoadDataWithBaseURL(supplied_base_url, data, supplied_history_url, title,
                      use_load_data_as_string_with_base_url());
  urls = GetURLsForLoadDataWithBaseURL(commit_url, supplied_base_url,
                                       supplied_history_url);
  CheckURLsMatchExpectationsForLoadDataWithBaseURL(
      controller.GetLastCommittedEntry(),
      static_cast<RenderFrameHostImpl*>(contents()->GetPrimaryMainFrame()),
      urls);

  // 2) Try to make a same-document navigation from the browser after
  // LoadDataWithBaseURL by navigating to the document URL + "#foo". This will
  // not be classified as same-document by the browser (unless the document URL
  // is the same as the commit URL, e.g. if the base URL is empty) because the
  // browser only compares against the document URL when deciding whether a
  // navigation is same-document or not.
  {
    SCOPED_TRACE(testing::Message() << " Testing case 2. ");

    FrameNavigateParamsCapturer capturer(root);
    capturer.set_wait_for_load(false);
    GURL document_url_with_fragment = GURL(urls.document_url.spec() + "#foo");
    NavigationController::LoadURLParams params(document_url_with_fragment);
    params.transition_type = ui::PageTransitionFromInt(
        ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
    contents()->GetController().LoadURLWithParams(params);
    capturer.Wait();
    EXPECT_EQ(urls.document_url == urls.commit_url,
              capturer.is_same_document());

    // Update the URL expectations for the regular commit.
    urls.commit_url = document_url_with_fragment;
    urls.document_url = document_url_with_fragment;
    urls.history_url_for_data_url = GURL();
    urls.supplied_base_url = GURL();
    // If the navigation is classified as a same document, it might use the
    // previous history URL as the virtual URL, probably unintentionally copied
    // from the previous NavigationEntry.
    urls.virtual_url = (capturer.is_same_document() && !history_url_empty())
                           ? supplied_history_url
                           : document_url_with_fragment;

    CheckURLsMatchExpectationsForLoadDataWithBaseURL(
        controller.GetLastCommittedEntry(),
        static_cast<RenderFrameHostImpl*>(contents()->GetPrimaryMainFrame()),
        urls);
  }
}

// Test history navigations on an iframe within a document loaded with
// loadDataWithBaseURL.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       LoadDataWithBaseURLThenHistoryNavigationInSubframe) {
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;

  GURL subframe_url_1(embedded_test_server()->GetURL(
      "a.com", "/navigation_controller/simple_page.html"));
  GURL subframe_url_2(embedded_test_server()->GetURL(
      "b.com", "/navigation_controller/simple_page_1.html"));
  const std::string data =
      base::StringPrintf("<html><body><iframe src=\"%s\"/></body></html>",
                         subframe_url_1.spec().c_str());

  // `base_url_for_data_url` in LoadURLParams.
  const GURL base_url = GURL("http://baseurl");
  // `history_url_for_data_url` in LoadURLParams.
  const GURL history_url = GURL("http://historyurl");
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  {
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    shell()->LoadDataWithBaseURL(history_url, data, base_url);
    same_tab_observer.Wait();

    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    scoped_refptr<FrameNavigationEntry> subframe_entry =
        entry->root_node()->children[0]->frame_entry.get();
    // The subframe's origin is based on the iframe's URL, unlike the main frame
    // which uses the base URL's origin in some cases.
    EXPECT_EQ(url::Origin::Create(subframe_url_1),
              subframe_entry->committed_origin());
    EXPECT_EQ(subframe_url_1, subframe_entry->url());
  }

  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  // 1) Do a same-document navigation via pushState in iframe.
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    std::string script = "history.pushState({}, 'page 1', '#bar1')";
    EXPECT_TRUE(ExecJs(root->child_at(0), script));
    capturer.Wait();
    EXPECT_TRUE(capturer.is_same_document());

    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    scoped_refptr<FrameNavigationEntry> main_frame_entry =
        entry->root_node()->frame_entry.get();
    // loadDataWithBaseURL() navigations uses the base URL's origin.
    EXPECT_EQ(url::Origin::Create(base_url),
              main_frame_entry->committed_origin());

    scoped_refptr<FrameNavigationEntry> subframe_entry =
        entry->root_node()->children[0]->frame_entry.get();
    // The subframe's origin is based on the iframe's URL, unlike the main frame
    // which uses the base URL's origin in some cases.
    EXPECT_EQ(url::Origin::Create(subframe_url_1),
              subframe_entry->committed_origin());
    EXPECT_EQ(subframe_url_1.spec() + "#bar1", subframe_entry->url());
  }

  // 2) Do a cross-document navigation on iframe.
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), subframe_url_2));
    capturer.Wait();
    EXPECT_FALSE(capturer.is_same_document());

    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    scoped_refptr<FrameNavigationEntry> main_frame_entry =
        entry->root_node()->frame_entry.get();
    // loadDataWithBaseURL() navigations uses the base URL's origin.
    EXPECT_EQ(url::Origin::Create(base_url),
              main_frame_entry->committed_origin());

    scoped_refptr<FrameNavigationEntry> subframe_entry =
        entry->root_node()->children[0]->frame_entry.get();
    // The subframe's origin is based on the iframe's URL, unlike the main frame
    // which uses the base URL's origin in some cases.
    EXPECT_EQ(url::Origin::Create(subframe_url_2),
              subframe_entry->committed_origin());
    EXPECT_EQ(subframe_url_2, subframe_entry->url());
  }

  // 3) Do a history navigation via GoBack. GoBack needs to be successful.
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    std::string script = "history.back()";
    EXPECT_TRUE(ExecJs(root->child_at(0), script));
    capturer.Wait();
    EXPECT_FALSE(capturer.is_same_document());
    EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    scoped_refptr<FrameNavigationEntry> main_frame_entry =
        entry->root_node()->frame_entry.get();
    // loadDataWithBaseURL() navigations uses the base URL's origin.
    EXPECT_EQ(url::Origin::Create(base_url),
              main_frame_entry->committed_origin());

    // Check the subframe origin is restored correctly.
    scoped_refptr<FrameNavigationEntry> subframe_entry =
        entry->root_node()->children[0]->frame_entry.get();
    EXPECT_EQ(url::Origin::Create(subframe_url_1),
              subframe_entry->committed_origin());
    EXPECT_EQ(subframe_url_1.spec() + "#bar1", subframe_entry->url());
  }
}

// Verify which page loads when going back to a LoadDataWithBaseURL entry.
// See https://crbug.com/612196.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       LoadDataWithBaseURLTitleAfterBack) {
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;

  const GURL base_url("http://baseurl");
  const GURL history_url(
      embedded_test_server()->GetURL("/navigation_controller/form.html"));
  const std::string data1 = "<html><title>One</title><body>foo</body></html>";
  const GURL data_url1 = GURL("data:text/html;charset=utf-8," + data1);

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  {
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    shell()->LoadDataWithBaseURL(history_url, data1, base_url);
    same_tab_observer.Wait();
  }

  // Verify the last committed NavigationEntry.
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(history_url, entry->GetVirtualURL());
  EXPECT_EQ(history_url, entry->GetHistoryURLForDataURL());
  EXPECT_EQ(data_url1, entry->GetURL());
  EXPECT_EQ("http://baseurl", EvalJs(shell(), "self.origin"));
  EXPECT_EQ(
      url::Origin::Create(base_url),
      shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());

  // Navigate again to a different data URL.
  const std::string data2 = "<html><title>Two</title><body>bar</body></html>";
  const GURL data_url2 = GURL("data:text/html;charset=utf-8," + data2);
  {
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    // Load data, not loaddatawithbaseurl.
    EXPECT_TRUE(NavigateToURL(shell(), data_url2));
    same_tab_observer.Wait();
  }
  url::Origin data_origin =
      shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin();
  EXPECT_TRUE(data_origin.opaque());
  EXPECT_EQ(url::SchemeHostPort(),
            data_origin.GetTupleOrPrecursorTupleIfOpaque());

  // Go back.
  TestNavigationObserver back_load_observer(shell()->web_contents());
  controller.GoBack();
  back_load_observer.Wait();

  // Check title.  We should load the data URL when going back.
  EXPECT_EQ("One", base::UTF16ToUTF8(shell()->web_contents()->GetTitle()));

  // Verify the last committed NavigationEntry.
  NavigationEntryImpl* back_entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, back_entry->GetBaseURLForDataURL());
  EXPECT_EQ(history_url, back_entry->GetVirtualURL());
  EXPECT_EQ(history_url, back_entry->GetHistoryURLForDataURL());
  EXPECT_EQ(data_url1, back_entry->GetOriginalRequestURL());
  EXPECT_EQ(data_url1, back_entry->GetURL());

  EXPECT_EQ(
      data_url1,
      shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
  EXPECT_EQ(
      url::Origin::Create(base_url),
      shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       CrossDomainResourceRequestLoadDataWithBaseUrl) {
  const GURL base_url("foobar://");
  const GURL history_url("http://historyurl");
  const std::string data = "<html><body></body></html>";
  const GURL data_url = GURL("data:text/html;charset=utf-8," + data);

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // Load data and commit.
  {
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    shell()->LoadDataWithBaseURL(history_url, data, base_url);
    same_tab_observer.Wait();
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
    EXPECT_EQ(history_url, entry->GetVirtualURL());
    EXPECT_EQ(history_url, entry->GetHistoryURLForDataURL());
    EXPECT_EQ(data_url, entry->GetURL());
  }

  // Now make an XHR request and check that the renderer isn't killed.
  std::string script =
      "var url = 'http://www.example.com';\n"
      "var xhr = new XMLHttpRequest();\n"
      "xhr.open('GET', url);\n"
      "xhr.send();\n";
  EXPECT_TRUE(ExecJs(shell()->web_contents(), script));
  // The renderer may not be killed immediately (if it is indeed killed), so
  // reload, block and verify its liveness.
  ReloadBlockUntilNavigationsComplete(shell(), 1);
  EXPECT_TRUE(
      shell()->web_contents()->GetPrimaryMainFrame()->IsRenderFrameLive());
}

class LoadDataWithBaseURLBrowserTest
    : public NavigationControllerBrowserTestBase,
      public testing::WithParamInterface<
          std::tuple<std::string /* render_document_level */,
                     bool /* use_load_data_as_string_with_base_url */>> {
 public:
  LoadDataWithBaseURLBrowserTest() {
    InitAndEnableRenderDocumentFeature(&feature_list_for_render_document_,
                                       std::get<0>(GetParam()));
  }

  // Provides meaningful param names instead of /0, /1, ...
  static std::string DescribeParams(
      const testing::TestParamInfo<ParamType>& info) {
    auto [render_document_level, use_load_data_as_string_with_base_url] =
        info.param;
    return base::StringPrintf(
        "%s_%s",
        GetRenderDocumentLevelNameForTestParams(render_document_level).c_str(),
        use_load_data_as_string_with_base_url ? "AsString" : "Normal");
  }

 protected:
  bool use_load_data_as_string_with_base_url() {
    return std::get<1>(GetParam());
  }

 private:
  base::test::ScopedFeatureList feature_list_for_render_document_;
};

// Tests that navigating with LoadDataWithBaseURL succeeds even when the base
// URL given is invalid.
IN_PROC_BROWSER_TEST_P(LoadDataWithBaseURLBrowserTest,
                       LoadDataWithInvalidBaseURL) {
#if !BUILDFLAG(IS_ANDROID)
  // LoadDataAsStringWithBaseURL is only supported on Android.
  if (use_load_data_as_string_with_base_url())
    return;
#endif
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;

  const GURL base_url("http://");  // Invalid.
  const GURL history_url("http://historyurl");
  const std::string title = "invalid_base_url";
  const std::string data = base::StringPrintf(
      "<html><head><title>%s</title></head><body>foo</body></html>",
      title.c_str());
  LoadDataWithBaseURL(base_url, data, history_url, title,
                      use_load_data_as_string_with_base_url());

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  // What the base URL ends up being is really implementation defined, as
  // using an invalid base URL is already undefined behavior.
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());

  const GURL original_committed_url =
      contents()->GetPrimaryMainFrame()->GetLastCommittedURL();

  {
    // Make a same-document navigation via history.pushState.
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    EXPECT_TRUE(ExecJs(shell(), "history.pushState('', 'test', '#foo')"));
    same_tab_observer.Wait();
  }

  // Verify that the same-document navigation succeeds.
  EXPECT_EQ(2, controller.GetEntryCount());
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(original_committed_url.spec() + "#foo",
            contents()->GetPrimaryMainFrame()->GetLastCommittedURL());

  // Verify that the page is not classified as an error page.
  EXPECT_EQ(PAGE_TYPE_NORMAL, entry->GetPageType());
  EXPECT_FALSE(contents()->GetPrimaryMainFrame()->IsErrorDocument());
}

// ContentBrowserClient that blocks normal commits to any URL in
// VerifyDidCommitParams.
class BlockAllCommitContentBrowserClient
    : public ContentBrowserTestContentBrowserClient {
 public:
  // Any visit to any URL will be blocked by VerifyDidCommitParams, except if
  // the checks are skipped (e.g. loadDataWithBaseURL).
  BlockAllCommitContentBrowserClient() = default;

  BlockAllCommitContentBrowserClient(
      const BlockAllCommitContentBrowserClient&) = delete;
  BlockAllCommitContentBrowserClient& operator=(
      const BlockAllCommitContentBrowserClient&) = delete;

  bool CanCommitURL(RenderProcessHost* process_host,
                    const GURL& site_url) override {
    return false;
  }
};

// Tests that navigating with LoadDataWithBaseURL succeeds when the data URL is
// typically blocked by an embedder, because it bypasses the renderer check.
IN_PROC_BROWSER_TEST_P(LoadDataWithBaseURLBrowserTest,
                       LoadDataWithBlockedDataURL) {
#if !BUILDFLAG(IS_ANDROID)
  // LoadDataAsStringWithBaseURL is only supported on Android.
  if (use_load_data_as_string_with_base_url())
    return;
#endif
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;

  BlockAllCommitContentBrowserClient content_browser_client;
  const GURL base_url = embedded_test_server()->GetURL("/title1.html");
  const GURL history_url("http://historyurl");
  const std::string title = "blocked_url";
  const std::string data = base::StringPrintf(
      "<html><head><title>%s</title></head><body>foo</body></html>",
      title.c_str());
  LoadDataWithBaseURL(base_url, data, history_url, title,
                      use_load_data_as_string_with_base_url());

  const GURL data_url = GURL("data:text/html;charset=utf-8," + data);
  const GURL commit_url = use_load_data_as_string_with_base_url()
                              ? GURL("data:text/html;charset=utf-8,")
                              : data_url;

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(commit_url,
            contents()->GetPrimaryMainFrame()->GetLastCommittedURL());

  // Verify that the page is not classified as an error page.
  EXPECT_EQ(PAGE_TYPE_NORMAL, entry->GetPageType());
  EXPECT_FALSE(contents()->GetPrimaryMainFrame()->IsErrorDocument());

  {
    // Make a same-document navigation via history.pushState.
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    EXPECT_TRUE(ExecJs(shell(), "history.pushState('', 'test', '#foo')"));
    same_tab_observer.Wait();
  }

  // Verify the last committed NavigationEntry.
  EXPECT_EQ(2, controller.GetEntryCount());
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(commit_url,
            contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
  {
    // Go back.
    TestNavigationObserver back_load_observer(shell()->web_contents());
    controller.GoBack();
    back_load_observer.Wait();
  }

  // Verify the last committed NavigationEntry.
  EXPECT_EQ(2, controller.GetEntryCount());
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(commit_url,
            contents()->GetPrimaryMainFrame()->GetLastCommittedURL());

  // Verify that the page is not classified as an error page.
  EXPECT_EQ(PAGE_TYPE_NORMAL, entry->GetPageType());
  EXPECT_FALSE(contents()->GetPrimaryMainFrame()->IsErrorDocument());

  {
    // Make a same-document navigation via fragment navigation.
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    EXPECT_TRUE(ExecJs(shell(), "location.href = '#bar';"));
    same_tab_observer.Wait();
  }

  // Verify the last committed NavigationEntry.
  EXPECT_EQ(2, controller.GetEntryCount());
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(commit_url,
            contents()->GetPrimaryMainFrame()->GetLastCommittedURL());

  // Verify that the page is not classified as an error page.
  EXPECT_EQ(PAGE_TYPE_NORMAL, entry->GetPageType());
  EXPECT_FALSE(contents()->GetPrimaryMainFrame()->IsErrorDocument());
}

// Tests that a LoadDataWithBaseURL to a blocked data URL and an invalid
// base URL results in a renderer kill, different from the
// LoadDataWithBlockedDataURL test above, because the navigation won't bypass
// the checks since the base URL is invalid.
IN_PROC_BROWSER_TEST_P(LoadDataWithBaseURLBrowserTest,
                       LoadDataWithBlockedDataURLAndInvalidBaseURL) {
#if !BUILDFLAG(IS_ANDROID)
  // LoadDataAsStringWithBaseURL is only supported on Android.
  if (use_load_data_as_string_with_base_url())
    return;
#endif
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;

  const GURL base_url("http://");  // Invalid.
  EXPECT_TRUE(!base_url.is_valid());
  const GURL history_url("http://historyurl");
  const std::string title = "invalid_base_url";
  const std::string data = base::StringPrintf(
      "<html><head><title>%s</title></head><body>foo</body></html>",
      title.c_str());

  BlockAllCommitContentBrowserClient content_browser_client;
  RenderProcessHostBadIpcMessageWaiter kill_waiter(
      shell()->web_contents()->GetPrimaryMainFrame()->GetProcess());

  if (use_load_data_as_string_with_base_url()) {
#if BUILDFLAG(IS_ANDROID)
    shell()->LoadDataAsStringWithBaseURL(history_url, data, base_url);
#else
    NOTREACHED();
#endif
  } else {
    shell()->LoadDataWithBaseURL(history_url, data, base_url);
  }

  // The renderer got killed because the ContentBrowserClient blocks the
  // navigation.
  EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait());
}

// Checks that a browser-initiated same-document navigation after a javascript:
// URL navigation on a page which has a valid base URL preserves the base URL.
IN_PROC_BROWSER_TEST_P(
    LoadDataWithBaseURLBrowserTest,
    LoadDataWithBaseURLThenJavaScriptURLThenSameDocumentNavigation) {
#if !BUILDFLAG(IS_ANDROID)
  // LoadDataAsStringWithBaseURL is only supported on Android.
  if (use_load_data_as_string_with_base_url())
    return;
#endif
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;

  const GURL base_url("http://baseurl");
  const GURL history_url("http://history");
  const std::string title = "foo";
  const std::string data_header = "data:text/html;charset=utf-8,";
  const std::string data = base::StringPrintf(
      "<html><head><title>%s</title></head><body>foo</body></html>",
      title.c_str());
  const GURL data_url = GURL(data_header + data);
  const GURL commit_url =
      use_load_data_as_string_with_base_url() ? GURL(data_header) : data_url;

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  LoadDataWithBaseURL(base_url, data, history_url, title,
                      use_load_data_as_string_with_base_url());

  // Verify the last committed NavigationEntry.
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(history_url, entry->GetVirtualURL());
  EXPECT_EQ(history_url, entry->GetHistoryURLForDataURL());
  EXPECT_EQ(commit_url, entry->GetURL());
  EXPECT_EQ(commit_url,
            contents()->GetPrimaryMainFrame()->GetLastCommittedURL());

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  const int64_t start_dsn = controller.GetLastCommittedEntry()
                                ->GetFrameEntry(root)
                                ->document_sequence_number();

  // Do a javascript: URL "navigation", which will create a new document but
  // won't send anything to the browser.
  EXPECT_TRUE(ExecJs(root, R"(window.location = 'javascript:"foo"';)"));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(
      use_load_data_as_string_with_base_url() ? GURL(data_header) : data_url,
      root->current_url());
  EXPECT_EQ("foo", EvalJs(shell(), "document.body.innerHTML"));
  EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
  EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
  EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                           ->GetFrameEntry(root)
                           ->document_sequence_number());

  {
    // Make a same-document navigation via history.pushState.
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    EXPECT_TRUE(ExecJs(shell(), "history.pushState('', 'test', '#foo')"));
    same_tab_observer.Wait();
  }

  // Verify the last committed NavigationEntry has the same URLs as it would
  // if the javascript: URL commit never happened.
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_NE(entry, controller.GetLastCommittedEntry());
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(history_url, entry->GetVirtualURL());
  EXPECT_EQ(history_url, entry->GetHistoryURLForDataURL());
  EXPECT_EQ(commit_url, entry->GetURL());
  EXPECT_EQ(commit_url,
            contents()->GetPrimaryMainFrame()->GetLastCommittedURL().spec());
  EXPECT_EQ(start_dsn, entry->GetFrameEntry(root)->document_sequence_number());

  {
    // Go back.
    TestNavigationObserver back_load_observer(shell()->web_contents());
    controller.GoBack();
    back_load_observer.Wait();
  }

  // Verify that we navigated back to the correct NavigationEntry.
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_NE(entry, controller.GetLastCommittedEntry());
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(history_url, entry->GetVirtualURL());
  EXPECT_EQ(history_url, entry->GetHistoryURLForDataURL());
  EXPECT_EQ(commit_url,
            contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
  EXPECT_EQ(start_dsn, entry->GetFrameEntry(root)->document_sequence_number());
}

// Tests that LoadDataWithBaseURL navigations that failed will commit an error
// page and correctly classified as an error page navigation.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ErrorPageFromLoadDataWithBaseURL) {
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // WebView or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;

  const GURL base_url("http://baseurl");
  const GURL history_url("http://historyurl");
  const std::string data = "<html><body></body></html>";
  const GURL data_url = GURL("data:text/html;charset=utf-8," + data);

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // Set up an URLLoaderInterceptor which will cause all navigations to fail.
  auto url_loader_interceptor = std::make_unique<URLLoaderInterceptor>(
      base::BindRepeating([](URLLoaderInterceptor::RequestParams* params) {
        network::URLLoaderCompletionStatus status;
        status.error_code = net::ERR_NOT_IMPLEMENTED;
        params->client->OnComplete(status);
        return true;
      }));

  // Do a LoadDataWithBaseURL navigation that will fail and commit an error
  // page instead.
  {
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    shell()->LoadDataWithBaseURL(history_url, data, base_url);
    same_tab_observer.Wait();

    EXPECT_FALSE(same_tab_observer.last_navigation_succeeded());
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();

    // Verify that the page is classified as an error page.
    EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
    EXPECT_TRUE(contents()->GetPrimaryMainFrame()->IsErrorDocument());

    EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
    EXPECT_EQ(history_url, entry->GetVirtualURL());
    EXPECT_EQ(history_url, entry->GetHistoryURLForDataURL());
    EXPECT_EQ(data_url, entry->GetURL());
  }
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NavigateFromLoadDataWithBaseURL) {
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;

  const GURL base_url("http://baseurl");
  const GURL history_url("http://historyurl");
  const std::string data = "<html><body></body></html>";
  const GURL data_url = GURL("data:text/html;charset=utf-8," + data);

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // Load data and commit.
  {
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    shell()->LoadDataWithBaseURL(history_url, data, base_url);
    same_tab_observer.Wait();
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
    EXPECT_EQ(history_url, entry->GetVirtualURL());
    EXPECT_EQ(history_url, entry->GetHistoryURLForDataURL());
    EXPECT_EQ(data_url, entry->GetURL());
  }

  // TODO(boliu): Add test for same document fragment navigation. See
  // crbug.com/561034.

  // Navigate with Javascript.
  {
    GURL navigate_url = embedded_test_server()->GetURL("/title1.html");
    std::string script = JsReplace("document.location = $1", navigate_url);
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    EXPECT_TRUE(ExecJs(shell(), script));
    same_tab_observer.Wait();
    EXPECT_EQ(2, controller.GetEntryCount());
    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(entry->GetBaseURLForDataURL().is_empty());
    EXPECT_TRUE(entry->GetHistoryURLForDataURL().is_empty());
    EXPECT_EQ(navigate_url, entry->GetVirtualURL());
    EXPECT_EQ(navigate_url, entry->GetURL());
  }
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FragmentNavigateFromLoadDataWithBaseURL) {
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;

  const GURL base_url("http://baseurl");
  const GURL history_url("http://historyurl");
  const std::string data =
      "<html><body>"
      "  <p id=\"frag\">"
      "    <a id=\"fraglink\" href=\"#frag\">same document nav</a>"
      "  </p>"
      "</body></html>";

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // Load data and commit.
  TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
#if BUILDFLAG(IS_ANDROID)
  shell()->LoadDataAsStringWithBaseURL(history_url, data, base_url);
#else
  shell()->LoadDataWithBaseURL(history_url, data, base_url);
#endif
  same_tab_observer.Wait();
  EXPECT_EQ(1, controller.GetEntryCount());
  const GURL data_url = controller.GetLastCommittedEntry()->GetURL();

  // Perform a fragment navigation using a javascript: URL (which doesn't lead
  // to a commit).
  GURL js_url("javascript:document.location = '#frag';");
  EXPECT_FALSE(NavigateToURL(shell(), js_url));
  EXPECT_EQ(2, controller.GetEntryCount());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(history_url, entry->GetHistoryURLForDataURL());
  EXPECT_EQ(history_url, entry->GetVirtualURL());
  EXPECT_EQ(data_url, entry->GetURL());

  // Passes if renderer is still alive.
  EXPECT_TRUE(ExecJs(shell(), "console.log('Success');"));
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, UniqueIDs) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_link_to_load_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  ASSERT_EQ(1, controller.GetEntryCount());

  // Use JavaScript to click the link and load the iframe.
  std::string script = "document.getElementById('link').click()";
  EXPECT_TRUE(ExecJs(shell(), script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  ASSERT_EQ(2, controller.GetEntryCount());

  // Unique IDs should... um... be unique.
  ASSERT_NE(controller.GetEntryAtIndex(0)->GetUniqueID(),
            controller.GetEntryAtIndex(1)->GetUniqueID());
}

// Ensures that RenderFrameHosts end up with the correct nav_entry_id() after
// navigations.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, UniqueIDsOnFrames) {
  NavigationController& controller = shell()->web_contents()->GetController();

  // Load a main frame with an about:blank subframe.
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(1U, root->child_count());
  ASSERT_NE(nullptr, root->child_at(0));

  // The main frame's nav_entry_id should match the last committed entry.
  int unique_id = controller.GetLastCommittedEntry()->GetUniqueID();
  EXPECT_EQ(unique_id, root->current_frame_host()->nav_entry_id());

  // The about:blank iframe should have inherited the same nav_entry_id.
  EXPECT_EQ(unique_id, root->child_at(0)->current_frame_host()->nav_entry_id());

  // Use NavigateToURLFromRenderer to go cross-site in the subframe.
  GURL foo_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), foo_url));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  // The unique ID should have stayed the same for the auto-subframe navigation,
  // since the new page replaces the initial about:blank page in the subframe.
  EXPECT_EQ(unique_id, controller.GetLastCommittedEntry()->GetUniqueID());
  EXPECT_EQ(unique_id, root->current_frame_host()->nav_entry_id());
  EXPECT_EQ(unique_id, root->child_at(0)->current_frame_host()->nav_entry_id());

  // Navigating in the subframe again should create a new entry.
  GURL foo_url2(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_2.html"));
  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), foo_url2));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  int unique_id2 = controller.GetLastCommittedEntry()->GetUniqueID();
  EXPECT_NE(unique_id, unique_id2);

  // The unique ID should have updated for the current RenderFrameHost in both
  // frames, not just the subframe.
  EXPECT_EQ(unique_id2, root->current_frame_host()->nav_entry_id());
  EXPECT_EQ(unique_id2,
            root->child_at(0)->current_frame_host()->nav_entry_id());
}

// This test used to make sure that a scheme used to prevent spoofs didn't ever
// interfere with navigations. We switched to a different scheme, so now this is
// just a test to make sure we can still navigate once we prune the history
// list.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       DontIgnoreBackAfterNavEntryLimit) {
  NavigationController& controller = shell()->web_contents()->GetController();

  // The default (50) makes this test too slow and leads to flakes
  // (https://crbug.com/1167300).
  NavigationControllerImpl::set_max_entry_count_for_testing(10);

  const int kMaxEntryCount =
      static_cast<int>(NavigationControllerImpl::max_entry_count());

  // Load up to the max count, all entries should be there.
  for (int url_index = 0; url_index < kMaxEntryCount; ++url_index) {
    GURL url(base::StringPrintf("data:text/html,page%d", url_index));
    EXPECT_TRUE(NavigateToURL(shell(), url));
  }

  EXPECT_EQ(controller.GetEntryCount(), kMaxEntryCount);

  // Navigate twice more.
  for (int url_index = kMaxEntryCount; url_index < kMaxEntryCount + 2;
       ++url_index) {
    GURL url(base::StringPrintf("data:text/html,page%d", url_index));
    EXPECT_TRUE(NavigateToURL(shell(), url));
  }

  // We expect page0 and page1 to be gone.
  EXPECT_EQ(kMaxEntryCount, controller.GetEntryCount());
  EXPECT_EQ(GURL("data:text/html,page2"),
            controller.GetEntryAtIndex(0)->GetURL());

  // Now try to go back. This should not hang.
  ASSERT_TRUE(controller.CanGoBack());
  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  // This should have successfully gone back.
  EXPECT_EQ(GURL(base::StringPrintf("data:text/html,page%d", kMaxEntryCount)),
            controller.GetLastCommittedEntry()->GetURL());
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       DontIgnoreRendererInitiatedBackAfterNavEntryLimit) {
  NavigationController& controller = shell()->web_contents()->GetController();

  // The default (50) makes this test too slow and leads to flakes
  // (https://crbug.com/1167300).
  NavigationControllerImpl::set_max_entry_count_for_testing(10);

  const int kMaxEntryCount =
      static_cast<int>(NavigationControllerImpl::max_entry_count());

  // Load up to the max count, all entries should be there.
  for (int url_index = 0; url_index < kMaxEntryCount; ++url_index) {
    GURL url(base::StringPrintf("data:text/html,page%d", url_index));
    EXPECT_TRUE(NavigateToURL(shell(), url));
  }

  EXPECT_EQ(controller.GetEntryCount(), kMaxEntryCount);

  // Navigate twice more.
  for (int url_index = kMaxEntryCount; url_index < kMaxEntryCount + 2;
       ++url_index) {
    GURL url(base::StringPrintf("data:text/html,page%d", url_index));
    EXPECT_TRUE(NavigateToURL(shell(), url));
  }

  // We expect page0 and page1 to be gone.
  EXPECT_EQ(kMaxEntryCount, controller.GetEntryCount());
  EXPECT_EQ(GURL("data:text/html,page2"),
            controller.GetEntryAtIndex(0)->GetURL());

  // Now try to go back. This should not hang.
  ASSERT_TRUE(controller.CanGoBack());
  {
    // Renderer-initiated-back.
    FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                              ->GetPrimaryFrameTree()
                              .root();
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.back()"));
    capturer.Wait();
  }

  // This should have successfully gone back.
  EXPECT_EQ(GURL(base::StringPrintf("data:text/html,page%d", kMaxEntryCount)),
            controller.GetLastCommittedEntry()->GetURL());
}

namespace {

// Does a renderer-initiated location.replace navigation to |url|, replacing the
// current entry.
bool RendererLocationReplace(Shell* shell, const GURL& url) {
  WebContents* web_contents = shell->web_contents();
  WaitForLoadStop(web_contents);
  TestNavigationManager navigation_manager(web_contents, url);
  const GURL& current_url =
      web_contents->GetPrimaryMainFrame()->GetLastCommittedURL();
  // Execute script in an isolated world to avoid causing a Trusted Types
  // violation due to eval.
  EXPECT_TRUE(ExecJs(shell, JsReplace("window.location.replace($1)", url),
                     EXECUTE_SCRIPT_DEFAULT_OPTIONS, /*world_id=*/1));
  // Observe pending entry if it's not a same-document navigation. We can't
  // observe same-document navigations because it might finish in the renderer,
  // only telling the browser side at the end.
  if (!current_url.EqualsIgnoringRef(url)) {
    EXPECT_TRUE(navigation_manager.WaitForRequestStart());
    EXPECT_TRUE(
        NavigationRequest::From(navigation_manager.GetNavigationHandle())
            ->common_params()
            .should_replace_current_entry);
  }
  EXPECT_TRUE(navigation_manager.WaitForNavigationFinished());
  if (!IsLastCommittedEntryOfPageType(web_contents, PAGE_TYPE_NORMAL))
    return false;
  return web_contents->GetLastCommittedURL() == url;
}

}  // namespace

// When loading a new page to replace an old page in the history list, make sure
// that the browser and renderer agree, and that both get it right.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       CorrectLengthWithCurrentItemReplacement) {
  NavigationController& controller = shell()->web_contents()->GetController();

  EXPECT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("/simple_page.html")));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(1, EvalJs(shell(), "history.length"));

  EXPECT_TRUE(RendererLocationReplace(
      shell(), embedded_test_server()->GetURL("/title1.html")));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(1, EvalJs(shell(), "history.length"));

  // Now create two more entries and go back, to test replacing an entry without
  // pruning the forward history.
  EXPECT_TRUE(
      NavigateToURL(shell(), embedded_test_server()->GetURL("/title2.html")));
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(2, EvalJs(shell(), "history.length"));

  EXPECT_TRUE(
      NavigateToURL(shell(), embedded_test_server()->GetURL("/title3.html")));
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(3, EvalJs(shell(), "history.length"));

  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_TRUE(controller.CanGoForward());

  EXPECT_TRUE(RendererLocationReplace(
      shell(), embedded_test_server()->GetURL("/simple_page.html?page1b")));
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(3, EvalJs(shell(), "history.length"));
  EXPECT_TRUE(controller.CanGoForward());

  // Note that there's no way to access the renderer's notion of the history
  // offset via JavaScript. Checking just the history length, though, is enough;
  // if the replacement failed, there would be a new history entry and thus an
  // incorrect length.
}

// When spawning a new page from a WebUI page, make sure that the browser and
// renderer agree about the length of the history list, and that both get it
// right.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       CorrectLengthWithNewTabNavigatingFromWebUI) {
  GURL web_ui_page(std::string(kChromeUIScheme) + "://" +
                   std::string(kChromeUIGpuHost));
  EXPECT_TRUE(NavigateToURL(shell(), web_ui_page));
  EXPECT_EQ(
      BINDINGS_POLICY_WEB_UI,
      shell()->web_contents()->GetPrimaryMainFrame()->GetEnabledBindings());

  ShellAddedObserver observer;
  GURL page_url = embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html");
  // Execute script in an isolated world to avoid causing a Trusted Types
  // violation due to eval.
  EXPECT_TRUE(ExecJs(shell(), JsReplace("window.open($1, '_blank');", page_url),
                     EXECUTE_SCRIPT_DEFAULT_OPTIONS, /*world_id=*/1));
  Shell* shell2 = observer.GetShell();
  EXPECT_TRUE(WaitForLoadStop(shell2->web_contents()));

  EXPECT_EQ(1, shell2->web_contents()->GetController().GetEntryCount());
  // Execute script in an isolated world to avoid causing a Trusted Types
  // violation due to eval.
  EXPECT_EQ(1, EvalJs(shell2, "history.length", EXECUTE_SCRIPT_DEFAULT_OPTIONS,
                      /*world_id=*/1));

  // Again, as above, there's no way to access the renderer's notion of the
  // history offset via JavaScript. Checking just the history length, again,
  // will have to suffice.
}

namespace {

class NoNavigationsObserver : public WebContentsObserver {
 public:
  // Observes navigation for the specified |web_contents|.
  explicit NoNavigationsObserver(WebContents* web_contents)
      : WebContentsObserver(web_contents) {}

 private:
  void DidFinishNavigation(NavigationHandle* navigation_handle) override {
    if (!navigation_handle->HasCommitted())
      return;
    FAIL() << "No navigations should occur";
  }
};

// Test that going back in a subframe on a loadDataWithBaseURL page doesn't
// crash.  See https://crbug.com/768575.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NavigateBackInChildOfLoadDataWithBaseURL) {
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;

  GURL iframe_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_links.html"));

  const GURL base_url("http://baseurl");
  const GURL history_url("http://historyurl");
  std::string data =
      "<html><body>"
      "  <p>"
      "    <iframe src=\"";
  data += iframe_url.spec();
  data +=
      "\" />"
      "  </p>"
      "</body></html>";

  // Load data and commit.
  TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
#if BUILDFLAG(IS_ANDROID)
  shell()->LoadDataAsStringWithBaseURL(history_url, data, base_url);
#else
  shell()->LoadDataWithBaseURL(history_url, data, base_url);
#endif
  same_tab_observer.Wait();

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(1u, root->child_count());
  FrameTreeNode* child = root->child_at(0u);

  {
    TestNavigationObserver observer(shell()->web_contents(), 1);
    std::string script = "document.getElementById('thelink').click()";
    EXPECT_TRUE(ExecJs(child, script));
    observer.Wait();
  }

  {
    TestNavigationObserver observer(shell()->web_contents(), 1);
    shell()->web_contents()->GetController().GoBack();
    observer.Wait();
  }

  // Passes if renderer is still alive.
  EXPECT_TRUE(ExecJs(shell(), "console.log('Success');"));
}

class LoadCommittedCapturer : public WebContentsObserver {
 public:
  // Observes the load commit for the specified |node|.
  explicit LoadCommittedCapturer(FrameTreeNode* node)
      : WebContentsObserver(
            WebContents::FromRenderFrameHost(node->current_frame_host())),
        frame_tree_node_id_(node->frame_tree_node_id()),
        message_loop_runner_(new MessageLoopRunner) {}

  // Observes the load commit for the next created frame in the specified
  // |web_contents|.
  explicit LoadCommittedCapturer(WebContents* web_contents)
      : WebContentsObserver(web_contents),
        frame_tree_node_id_(0),
        message_loop_runner_(new MessageLoopRunner) {}

  void Wait() { message_loop_runner_->Run(); }

  ui::PageTransition transition_type() const { return transition_type_; }

 private:
  void RenderFrameCreated(RenderFrameHost* render_frame_host) override {
    RenderFrameHostImpl* rfh =
        static_cast<RenderFrameHostImpl*>(render_frame_host);

    // Don't pay attention to pending delete RenderFrameHosts in the main frame,
    // which might happen in a race if a cross-process navigation happens
    // quickly.
    if (rfh->IsPendingDeletion()) {
      DLOG(INFO) << "Skipping pending delete RFH: "
                 << rfh->GetSiteInstance()->GetSiteURL();
      return;
    }

    // If this object was not created with a specified frame tree node, then use
    // the first created active RenderFrameHost.  Once a node is selected, there
    // shouldn't be any other frames being created.
    int frame_tree_node_id = rfh->frame_tree_node()->frame_tree_node_id();
    DCHECK(frame_tree_node_id_ == 0 ||
           frame_tree_node_id_ == frame_tree_node_id);
    frame_tree_node_id_ = frame_tree_node_id;
  }

  void DidFinishNavigation(NavigationHandle* navigation_handle) override {
    if (!navigation_handle->HasCommitted())
      return;

    DCHECK_NE(0, frame_tree_node_id_);
    if (navigation_handle->GetRenderFrameHost()->GetFrameTreeNodeId() !=
        frame_tree_node_id_) {
      return;
    }

    transition_type_ = navigation_handle->GetPageTransition();
    if (!web_contents()->IsLoading())
      message_loop_runner_->Quit();
  }

  void DidStopLoading() override { message_loop_runner_->Quit(); }

  // The id of the FrameTreeNode whose navigations to observe.
  int frame_tree_node_id_;

  // The transition_type of the last navigation.
  ui::PageTransition transition_type_;

  // The MessageLoopRunner used to spin the message loop.
  scoped_refptr<MessageLoopRunner> message_loop_runner_;
};

}  // namespace

// Some pages create a popup, then write an iframe into it. This used to cause a
// subframe navigation without having any committed entry. Such navigations
// used to just get thrown on the ground, but we shouldn't crash.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, SubframeOnEmptyPage) {
  // Navigate to a page to force the renderer process to start.
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));

  // Pop open a new blank window.
  Shell* new_shell = OpenBlankWindow(contents());
  FrameTreeNode* new_root =
      static_cast<WebContentsImpl*>(new_shell->web_contents())
          ->GetPrimaryFrameTree()
          .root();
  EXPECT_TRUE(new_shell->web_contents()
                  ->GetController()
                  .GetLastCommittedEntry()
                  ->IsInitialEntry());

  // Make a new iframe in it.
  {
    LoadCommittedCapturer capturer(new_shell->web_contents());
    std::string script =
        JsReplace(kAddFrameWithSrcScript, "data:text/html,<p>some page</p>");
    EXPECT_TRUE(ExecJs(new_root, script));
    capturer.Wait();
  }
  ASSERT_EQ(1U, new_root->child_count());
  ASSERT_NE(nullptr, new_root->child_at(0));

  // The subframe navigation should not be ignored, and we will create a
  // FrameNavigationEntry for the new iframe.
  EXPECT_TRUE(
      static_cast<NavigationEntryImpl*>(
          new_shell->web_contents()->GetController().GetLastCommittedEntry())
          ->GetFrameEntry(new_root->child_at(0)));

  // Navigate it cross-site.
  GURL frame_url = embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_2.html");
  {
    LoadCommittedCapturer capturer(new_root->child_at(0));
    std::string script = JsReplace("location.assign($1);", frame_url);
    EXPECT_TRUE(ExecJs(new_root->child_at(0), script));
    capturer.Wait();
  }

  // The subframe navigation should not ignored, and we will create a
  // FrameNavigationEntry for the new iframe.
  EXPECT_TRUE(
      static_cast<NavigationEntryImpl*>(
          new_shell->web_contents()->GetController().GetLastCommittedEntry())
          ->GetFrameEntry(new_root->child_at(0)));

  // A nested iframe with a cross-site URL should also be able to commit.
  GURL grandchild_url(embedded_test_server()->GetURL(
      "bar.com", "/navigation_controller/simple_page_1.html"));
  {
    LoadCommittedCapturer capturer(new_shell->web_contents());
    EXPECT_TRUE(ExecJs(new_root->child_at(0),
                       JsReplace(kAddFrameWithSrcScript, grandchild_url)));
    capturer.Wait();
  }
  ASSERT_EQ(1U, new_root->child_at(0)->child_count());
  EXPECT_EQ(grandchild_url, new_root->child_at(0)->child_at(0)->current_url());
  // The subframe navigation should not be ignored, and we will create a
  // FrameNavigationEntry for the new grandchild iframe.
  EXPECT_EQ(
      grandchild_url,
      static_cast<NavigationEntryImpl*>(
          new_shell->web_contents()->GetController().GetLastCommittedEntry())
          ->GetFrameEntry(new_root->child_at(0)->child_at(0))
          ->url());
}

// Test that the renderer is not killed after an auto subframe navigation if the
// main frame appears to change its origin due to a document.write on an
// about:blank page.  See https://crbug.com/613732.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       OriginChangeAfterDocumentWrite) {
  GURL url1 = embedded_test_server()->GetURL("/title1.html");
  GURL url2 = embedded_test_server()->GetURL("/title2.html");
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Pop open a new window to `url2`.
  ShellAddedObserver new_shell_observer;
  EXPECT_TRUE(ExecJs(root, "var w = window.open('" + url2.spec() + "');"));
  Shell* new_shell = new_shell_observer.GetShell();
  ASSERT_NE(new_shell->web_contents(), shell()->web_contents());
  EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
  FrameTreeNode* new_root =
      static_cast<WebContentsImpl*>(new_shell->web_contents())
          ->GetPrimaryFrameTree()
          .root();

  // Navigate the new window to about:blank. Note that we didn't directly
  // window.open() to about:blank which because otherwise the about:blank page
  // will always get replaced on the next navigation.
  GURL blank_url(url::kAboutBlankURL);
  {
    TestNavigationObserver nav_observer(new_shell->web_contents());
    EXPECT_TRUE(ExecJs(new_shell, "location.href = 'about:blank';"));
    nav_observer.Wait();
    EXPECT_EQ(2, new_shell->web_contents()->GetController().GetEntryCount());
    EXPECT_EQ(blank_url, new_root->current_url());
    EXPECT_EQ(blank_url,
              new_root->current_frame_host()->last_document_url_in_renderer());
  }

  // Make a new iframe with src=`url2` in it using document.write from the
  // opener.
  {
    LoadCommittedCapturer capturer(new_shell->web_contents());
    std::string html = "<iframe src='" + url2.spec() + "'></iframe>";
    std::string script = JsReplace(
        "w.document.write($1);"
        "w.document.close();",
        html);
    EXPECT_TRUE(ExecJs(root->current_frame_host(), script));
    capturer.Wait();
  }
  ASSERT_EQ(1U, new_root->child_count());
  // The main frame stays at "about:blank", but due to document.write(), its
  // "document URL" is set to the opener URL.
  EXPECT_EQ(blank_url, new_root->current_url());
  EXPECT_EQ(url1,
            new_root->current_frame_host()->last_document_url_in_renderer());
  EXPECT_EQ(url2, new_root->child_at(0)->current_url());

  // Navigate the subframe to `url3`.
  GURL url3 = embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html");
  {
    LoadCommittedCapturer capturer(new_root->child_at(0));
    std::string script = "location.href = '" + url3.spec() + "';";
    EXPECT_TRUE(ExecJs(new_root->child_at(0), script));
    capturer.Wait();
  }
  EXPECT_EQ(blank_url, new_root->current_url());
  EXPECT_EQ(url1,
            new_root->current_frame_host()->last_document_url_in_renderer());
  EXPECT_EQ(url3, new_root->child_at(0)->current_url());
  EXPECT_EQ(3, new_shell->web_contents()->GetController().GetEntryCount());

  // Do a replaceState() in the main frame, which changes the URL from
  // about:blank to a URL with the opener's origin, because replaceState uses
  // the "document URL" when resolving relative URLs.
  {
    LoadCommittedCapturer capturer(new_root);
    std::string script = "history.replaceState({}, 'foo', 'foo');";
    EXPECT_TRUE(ExecJs(new_root, script));
    capturer.Wait();
  }
  GURL replace_state_url(embedded_test_server()->GetURL("/foo"));
  EXPECT_EQ(replace_state_url, new_root->current_url());
  EXPECT_EQ(replace_state_url,
            new_root->current_frame_host()->last_document_url_in_renderer());
  EXPECT_EQ(url3, new_root->child_at(0)->current_url());

  // Go back in the subframe.  Note that in the past before we share FNEs
  // between NavigationEntries (and update all affected NavigationEntry on
  // replaceState etc) the main frame's URL looked like a cross-origin change
  // from a web URL to about:blank, but now it's consistently
  // `replace_state_url` everywhere.
  {
    TestNavigationObserver observer(new_shell->web_contents(), 1);
    new_shell->web_contents()->GetController().GoBack();
    observer.Wait();
    EXPECT_EQ(replace_state_url, new_shell->web_contents()
                                     ->GetController()
                                     .GetLastCommittedEntry()
                                     ->GetURL());
    EXPECT_EQ(replace_state_url, new_root->current_url());
    EXPECT_EQ(replace_state_url,
              new_root->current_frame_host()->last_document_url_in_renderer());
    EXPECT_EQ(url2, new_root->child_at(0)->current_url());
  }
  EXPECT_TRUE(new_root->current_frame_host()->IsRenderFrameLive());

  // Go forward in the subframe.  Note that in the past before we share FNEs
  // between NavigationEntries (and update all affected NavigationEntry on
  // replaceState etc) the main frame's URL looked like a cross-origin change
  // from a web URL to about:blank, but now it's consistently
  // `replace_state_url` everywhere.
  {
    TestNavigationObserver observer(new_shell->web_contents(), 1);
    new_shell->web_contents()->GetController().GoForward();
    observer.Wait();
    EXPECT_EQ(replace_state_url, new_shell->web_contents()
                                     ->GetController()
                                     .GetLastCommittedEntry()
                                     ->GetURL());
    EXPECT_EQ(replace_state_url, new_root->current_url());
    EXPECT_EQ(url3, new_root->child_at(0)->current_url());
  }
  EXPECT_TRUE(new_root->current_frame_host()->IsRenderFrameLive());
}

// Test that a frame's url is correctly updated after a document.open() from
// another frame.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       DocumentOpenFromSibling) {
  // Open a page with iframe.
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe_simple.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  EXPECT_EQ(1U, root->child_count());

  GURL first_frame_url = root->child_at(0)->current_url();
  GURL second_frame_url(embedded_test_server()->GetURL("/title2.html"));
  // Make a new iframe.
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(
        ExecJs(root, JsReplace(kAddFrameWithSrcScript, second_frame_url)));
    capturer.Wait();
  }
  EXPECT_EQ(2U, root->child_count());
  // Call document.open() on the first iframe from the second iframe.
  EXPECT_TRUE(ExecJs(
      root->child_at(1),
      "parent.document.getElementById(\"frame\").contentDocument.open();"));
  // Run script in the iframes to ensure that the URL update is already sent
  // to the browser before continuing.
  EXPECT_TRUE(ExecJs(root->child_at(1), "true"));
  EXPECT_TRUE(ExecJs(root->child_at(0), "true"));

  RenderFrameHostImpl* child0 = root->child_at(0)->current_frame_host();
  RenderFrameHostImpl* child1 = root->child_at(1)->current_frame_host();
  // The value of "last committed URL" in child0 is not updated, but "last
  // document URL" is updated correctly to be the same as the child1's URL.
  EXPECT_EQ(first_frame_url, child0->GetLastCommittedURL());
  EXPECT_EQ(second_frame_url, child0->last_document_url_in_renderer());
  EXPECT_EQ(second_frame_url, child1->GetLastCommittedURL());
  EXPECT_EQ(second_frame_url, child1->last_document_url_in_renderer());
}

// Test that a frame's url is correctly updated after a document.open() from
// an about:blank frame.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       DocumentOpenFromAboutBlank) {
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe_simple.html"));
  GURL frame_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();

  // Make a new about:blank iframe that will document.open() its sibling.
  {
    LoadCommittedCapturer capturer(contents());
    EXPECT_EQ("done", EvalJs(root->current_frame_host(), R"(
      new Promise(async resolve => {
        const blank_iframe = document.createElement('iframe');
        await new Promise(resolve => {
          blank_iframe.onload = resolve;
          document.body.appendChild(blank_iframe);
        });

        let script = document.createElement('script');
        script.text = `
          const sibling = parent.document.getElementById("frame")
          sibling.contentDocument.open();
        `;
        blank_iframe.contentDocument.body.appendChild(script);
        resolve("done");
      })
    )"));
    capturer.Wait();
  }
  // Run script in the iframes to ensure that the URL update is already sent
  // to the browser before continuing.
  EXPECT_TRUE(ExecJs(root->child_at(1), "true"));
  EXPECT_TRUE(ExecJs(root->child_at(0), "true"));

  ASSERT_EQ(2U, root->child_count());
  RenderFrameHostImpl* child0 = root->child_at(0)->current_frame_host();
  RenderFrameHostImpl* child1 = root->child_at(1)->current_frame_host();
  // The value of "last committed URL" in child0 is not updated, but "last
  // document URL" is updated correctly to "about:blank".
  EXPECT_EQ(frame_url, child0->GetLastCommittedURL());
  EXPECT_EQ(GURL(url::kAboutBlankURL), child0->last_document_url_in_renderer());
  EXPECT_EQ(GURL(url::kAboutBlankURL), child1->GetLastCommittedURL());
  EXPECT_EQ(GURL(url::kAboutBlankURL), child1->last_document_url_in_renderer());
}

// Test that a frame's url is partially updated after a document.open() from
// an about:srcdoc frame.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       DocumentOpenFromSrcdoc) {
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe_simple.html"));
  GURL frame_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();

  // Make a new about:srcdoc iframe that will document.open() its sibling.
  {
    LoadCommittedCapturer capturer(contents());
    std::string script =
        "let origin = document.createElement('iframe');"
        "origin.srcdoc = '<script>parent.document.getElementById(\"frame\")"
        ".contentDocument.open();</script>';"
        "document.body.appendChild(origin);";
    EXPECT_TRUE(ExecJs(root->current_frame_host(), script));
    capturer.Wait();
  }
  // Run script in the iframes to ensure that the URL update is already sent
  // to the browser before continuing.
  EXPECT_TRUE(ExecJs(root->child_at(1), "true"));
  EXPECT_TRUE(ExecJs(root->child_at(0), "true"));

  ASSERT_EQ(2U, root->child_count());
  RenderFrameHostImpl* child0 = root->child_at(0)->current_frame_host();
  RenderFrameHostImpl* child1 = root->child_at(1)->current_frame_host();
  // The value of "last committed URL" in child0 is not updated, but "last
  // document URL" is updated correctly to "about:srdoc".
  EXPECT_EQ(frame_url, child0->GetLastCommittedURL());
  EXPECT_EQ(GURL("about:srcdoc"), child0->last_document_url_in_renderer());
  EXPECT_EQ(GURL("about:srcdoc"), child1->GetLastCommittedURL());
  EXPECT_EQ(GURL("about:srcdoc"), child1->last_document_url_in_renderer());
}

// Test that a frame's url is partially updated after a document.open() from
// a blob: url.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       DocumentOpenFromBloblIframe) {
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe_simple.html"));
  GURL frame_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();

  // Make a new blob iframe that will document.open() its sibling.
  {
    LoadCommittedCapturer capturer(contents());
    std::string script =
        "let origin = document.createElement('iframe');"
        "let blob = new Blob(['<script>"
        "parent.document.getElementById(\"frame\").contentDocument.open();"
        "</script>'], { type: 'text/html' });"
        "origin.src = URL.createObjectURL(blob);"
        "document.body.appendChild(origin);";
    EXPECT_TRUE(ExecJs(root->current_frame_host(), script));
    capturer.Wait();
  }
  // Run script in the iframes to ensure that the URL update is already sent
  // to the browser before continuing.
  EXPECT_TRUE(ExecJs(root->child_at(1), "true"));
  EXPECT_TRUE(ExecJs(root->child_at(0), "true"));

  ASSERT_EQ(2U, root->child_count());
  RenderFrameHostImpl* child0 = root->child_at(0)->current_frame_host();
  RenderFrameHostImpl* child1 = root->child_at(1)->current_frame_host();
  // The value of "last committed URL" in child0 is not updated, but "last
  // document URL" is updated correctly to the blob URL.
  EXPECT_EQ(frame_url, child0->GetLastCommittedURL());
  EXPECT_TRUE(child0->last_document_url_in_renderer().SchemeIsBlob());
  EXPECT_TRUE(child1->GetLastCommittedURL().SchemeIsBlob());
  EXPECT_TRUE(child1->last_document_url_in_renderer().SchemeIsBlob());
  EXPECT_EQ(child0->last_document_url_in_renderer(),
            child1->last_document_url_in_renderer());
}

// Test the last committed, document, and history URLs in various cases.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, RendererURLs) {
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe_simple.html"));
  GURL iframe_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  // 1) Navigate to `url1`. All three URLs in the main frame should be set to
  // `url_1`, and `frame_url` in the iframe.
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  EXPECT_EQ(url1, root->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(url1, root->current_frame_host()->last_document_url_in_renderer());
  FrameTreeNode* iframe = root->child_at(0);
  EXPECT_EQ(iframe_url, iframe->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(iframe_url,
            iframe->current_frame_host()->last_document_url_in_renderer());

  // 2) Do a document.open() on the iframe from the main frame.
  EXPECT_TRUE(ExecJs(
      root, "document.getElementById(\"frame\").contentDocument.open();"));
  // Run script in both the main frame and the iframe to ensure that the URL
  // update is already sent to the browser before continuing.
  EXPECT_TRUE(ExecJs(root, "true"));
  EXPECT_TRUE(ExecJs(iframe, "true"));
  // THe iframe's document & history URL is updated to `url1`.
  EXPECT_EQ(iframe_url, iframe->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(url1,
            iframe->current_frame_host()->last_document_url_in_renderer());

  // 3) Do a same-document navigation to `url_1_fragment` on the iframe (Note
  // that this is a same-document navigation because the iframe's document URL
  // is now `url1`).
  GURL url_1_fragment(url1.spec() + "#foo");
  {
    FrameNavigateParamsCapturer capturer(iframe);
    capturer.set_wait_for_load(false);
    EXPECT_TRUE(
        ExecJs(iframe, JsReplace("location.href = '#foo';", url_1_fragment)));
    capturer.Wait();
    EXPECT_TRUE(capturer.is_same_document());
  }
  // All three URLs in the iframe should be updated to `url1_fragment`.
  EXPECT_EQ(url_1_fragment,
            iframe->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(url_1_fragment,
            iframe->current_frame_host()->last_document_url_in_renderer());

  // 4) Do a navigation to `url404`, which wil result in an error page.
  // The document URL will be set to the kUnreachableWebDataURL.
  GURL url404(embedded_test_server()->GetURL("/empty404.html"));
  EXPECT_FALSE(NavigateToURL(shell(), url404));
  EXPECT_EQ(url404, root->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(GURL(kUnreachableWebDataURL),
            root->current_frame_host()->last_document_url_in_renderer());

  // 5) Do a same-document pushState on an error page without changing the URL
  // (otherwise it will result in an origin mismatch error). The URLs will stay
  // the same.
  {
    FrameNavigateParamsCapturer capturer(root);
    capturer.set_wait_for_load(false);
    EXPECT_TRUE(ExecJs(shell(), "history.pushState('', '')"));
    capturer.Wait();
    EXPECT_TRUE(capturer.is_same_document());
  }
  EXPECT_EQ(url404, root->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(GURL(kUnreachableWebDataURL),
            root->current_frame_host()->last_document_url_in_renderer());
}

// Tests various cases of replacements caused by error pages.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, ErrorPageReplacement) {
  NavigationController& controller = shell()->web_contents()->GetController();
  GetIOThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&net::URLRequestFailedJob::AddUrlHandler));

  GURL url1 = embedded_test_server()->GetURL("/title1.html");
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  EXPECT_EQ(1, controller.GetEntryCount());

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1) Navigate to a page that fails to load. It must result in an error page,
  // the NEW_ENTRY navigation type, and an addition to the history list.
  GURL error_url = embedded_test_server()->GetURL("/close-socket");
  {
    FrameNavigateParamsCapturer capturer(root);
    NavigateFrameToURL(root, error_url);
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
    EXPECT_EQ(2, controller.GetEntryCount());
  }

  // 2) Navigate again to the same URL. It results in an error page, the
  // NEW_ENTRY navigation type, does replacement, and no addition to the
  // history list. The navigation initially got converted into a reload by the
  // browser but since it failed and doesn't have a valid PageState, it does
  // replacement instead.
  {
    FrameNavigateParamsCapturer capturer(root);
    NavigateFrameToURL(root, error_url);
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
    EXPECT_EQ(2, controller.GetEntryCount());
  }

  // 3) Navigate to a different URL (|url2|) that fails to load. It adds a new
  // entry.
  GURL url2 = embedded_test_server()->GetURL("/title2.html");
  {
    // Set up an URLLoaderInterceptor which will cause the next navigation to
    // fail.
    auto url_loader_interceptor = std::make_unique<URLLoaderInterceptor>(
        base::BindRepeating([](URLLoaderInterceptor::RequestParams* params) {
          network::URLLoaderCompletionStatus status;
          status.error_code = net::ERR_NOT_IMPLEMENTED;
          params->client->OnComplete(status);
          return true;
        }));

    FrameNavigateParamsCapturer capturer(root);
    NavigateFrameToURL(root, url2);
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_FALSE(capturer.did_replace_entry());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  }

  // 4) Go back to the previous error page. Since this still results in an error
  // page, it's classified as EXISTING_ENTRY, like a normal history navigation.
  {
    FrameNavigateParamsCapturer capturer(root);
    controller.GoBack();
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_FALSE(capturer.did_replace_entry());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  }

  // 5) Go forward to |url2|. This navigation will succeed, and since the final
  // SiteInstance is different from the history entry's (due to error page
  // isolation), it will be classified as NEW_ENTRY with replacement.
  {
    FrameNavigateParamsCapturer capturer(root);
    controller.GoForward();
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_NE(PAGE_TYPE_ERROR, entry->GetPageType());
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  }

  // 6) Make a new entry ...
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  EXPECT_EQ(4, controller.GetEntryCount());

  // 7) ... and replace it with a failed load.
  {
    FrameNavigateParamsCapturer capturer(root);
    RendererLocationReplace(shell(), error_url);
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
    EXPECT_EQ(4, controller.GetEntryCount());
  }

  // 8) Make a new web ui page to force a process swap ...
  GURL web_ui_page(std::string(kChromeUIScheme) + "://" +
                   std::string(kChromeUIGpuHost));
  EXPECT_TRUE(NavigateToURL(shell(), web_ui_page));
  EXPECT_EQ(5, controller.GetEntryCount());

  // 9) ... and replace it with a failed load. (It is NEW_ENTRY for the reason
  // noted above.)
  {
    FrameNavigateParamsCapturer capturer(root);
    RendererLocationReplace(shell(), error_url);
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
    EXPECT_EQ(5, controller.GetEntryCount());
  }
}

// Tests various cases of replacements caused by error pages on subframes.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ErrorPageReplacementSubframe) {
  NavigationController& controller = shell()->web_contents()->GetController();
  // Navigate to a page with an iframe.
  GURL main_frame_url = embedded_test_server()->GetURL(
      "/navigation_controller/page_with_data_iframe.html");
  EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
  EXPECT_EQ(1, controller.GetEntryCount());

  GURL error_url = embedded_test_server()->GetURL("/close-socket");
  GetIOThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&net::URLRequestFailedJob::AddUrlHandler));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1) Navigate the subframe to a page that fails to load. It must result in an
  // error page, the NEW_SUBFRAME navigation type, and an addition to the
  // history list.
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    NavigateFrameToURL(root->child_at(0), error_url);
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
    EXPECT_EQ(2, controller.GetEntryCount());
  }

  // 2) Navigate the subframe again to the same URL. It results in an error
  // page, the AUTO_SUBFRAME navigation type, does replacement, and no addition
  // to the history list. This is because the navigation got converted into a
  // reload and doesn't have a valid PageState, but since it fails,
  // it does replacement instead of reload.
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    NavigateFrameToURL(root->child_at(0), error_url);
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_EQ(2, controller.GetEntryCount());
  }

  // 3) Navigate the subframe to a different URL (|url2|) that fails to load. It
  // adds a new entry.
  GURL url2 = embedded_test_server()->GetURL("/title1.html");
  {
    // Set up an URLLoaderInterceptor which will cause the next navigation to
    // fail.
    auto url_loader_interceptor = std::make_unique<URLLoaderInterceptor>(
        base::BindRepeating([](URLLoaderInterceptor::RequestParams* params) {
          network::URLLoaderCompletionStatus status;
          status.error_code = net::ERR_NOT_IMPLEMENTED;
          params->client->OnComplete(status);
          return true;
        }));

    FrameNavigateParamsCapturer capturer(root->child_at(0));
    NavigateFrameToURL(root->child_at(0), url2);
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  }

  // 4) Go back to the previous error page on the subframe. Since this still
  // results in an error page, it's classified as AUTO_SUBFRAME and won't do
  // replacement, like a normal subframe history navigation.
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    controller.GoBack();
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  }

  // 5) Go forward to |url2|. This navigation will succeed, and since the final
  // SiteInstance is the same as the entry, it will be classified as
  // AUTO_SUBFRAME and won't do replacement, like a normal subframe history
  // navigation. Note this is different from the main frame case, where the
  // navigation will do a replacement instead, because the SiteInstance would be
  // different due to error page isolation on main frames.
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    controller.GoForward();
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  }

  // 6) Navigate the subframe to the same URL but fail to load again. It will
  // be classified as AUTO_SUBFRAME and do replacement.
  {
    // Set up an URLLoaderInterceptor which will cause the next navigation to
    // fail.
    auto url_loader_interceptor = std::make_unique<URLLoaderInterceptor>(
        base::BindRepeating([](URLLoaderInterceptor::RequestParams* params) {
          network::URLLoaderCompletionStatus status;
          status.error_code = net::ERR_NOT_IMPLEMENTED;
          params->client->OnComplete(status);
          return true;
        }));

    FrameNavigateParamsCapturer capturer(root->child_at(0));
    NavigateFrameToURL(root->child_at(0), url2);
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  }
}

// Various tests for navigation type classifications. TODO(avi): It's rather
// bogus that the same info is in two different enums; http://crbug.com/453555.

// Verify that navigations for NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY are
// correctly classified.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NavigationTypeClassification_NewEntry) {
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  {
    // Simple load.
    FrameNavigateParamsCapturer capturer(root);
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/page_with_links.html"));
    NavigateFrameToURL(root, frame_url);
    capturer.Wait();
    // TODO(avi,creis): Why is this (and quite a few others below) a "link"
    // transition? Lots of these transitions should be cleaned up.
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_LINK));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());
  }

  NavigationEntryImpl* previous_entry = controller.GetLastCommittedEntry();
  scoped_refptr<FrameNavigationEntry> previous_root_entry =
      previous_entry->root_node()->frame_entry.get();

  {
    // Load via a fragment link click.
    FrameNavigateParamsCapturer capturer(root);
    std::string script = "document.getElementById('fraglink').click()";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_LINK));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());

    // The FrameNavigationEntry and NavigationEntry are not reused.
    EXPECT_NE(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
    previous_entry = controller.GetLastCommittedEntry();
    previous_root_entry = previous_entry->root_node()->frame_entry.get();
  }

  {
    // Load via link click.
    FrameNavigateParamsCapturer capturer(root);
    std::string script = "document.getElementById('thelink').click()";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_LINK));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());

    // The FrameNavigationEntry and NavigationEntry are not reused.
    EXPECT_NE(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
    previous_entry = controller.GetLastCommittedEntry();
    previous_root_entry = previous_entry->root_node()->frame_entry.get();
  }

  {
    // location.assign().
    FrameNavigateParamsCapturer capturer(root);
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_2.html"));
    std::string script = JsReplace("location.assign($1);", frame_url);
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_LINK));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());

    // The FrameNavigationEntry and NavigationEntry are not reused.
    EXPECT_NE(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
    previous_entry = controller.GetLastCommittedEntry();
    previous_root_entry = previous_entry->root_node()->frame_entry.get();
  }

  {
    // history.pushState().
    FrameNavigateParamsCapturer capturer(root);
    std::string script =
        "history.pushState({}, 'page 1', 'simple_page_1.html')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_LINK));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());

    // The FrameNavigationEntry and NavigationEntry are not reused.
    EXPECT_NE(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
    previous_entry = controller.GetLastCommittedEntry();
    previous_root_entry = previous_entry->root_node()->frame_entry.get();
  }

  // location.replace().
  {
    FrameNavigateParamsCapturer capturer(root);
    GURL frame_url(embedded_test_server()->GetURL(
        "foo.com", "/navigation_controller/simple_page_1.html"));
    std::string script = JsReplace("location.replace($1);", frame_url);
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
                                  ui::PAGE_TRANSITION_CLIENT_REDIRECT)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_FALSE(capturer.is_same_document());

    // The FrameNavigationEntry and NavigationEntry are not reused.
    EXPECT_NE(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
  }
}

// Verify that navigations for NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY are
// correctly classified.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NavigationTypeClassification_ExistingEntry) {
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url2));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  {
    // Back from the browser side.
    FrameNavigateParamsCapturer capturer(root);
    controller.GoBack();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FORWARD_BACK |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());
  }

  {
    // Forward from the browser side.
    FrameNavigateParamsCapturer capturer(root);
    controller.GoForward();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FORWARD_BACK |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());
  }

  {
    // Back from the renderer side.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.back()"));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FORWARD_BACK |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());
  }

  {
    // Forward from the renderer side.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.forward()"));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FORWARD_BACK |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());
  }

  {
    // Back from the renderer side via history.go().
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.go(-1)"));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FORWARD_BACK |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());
  }

  {
    // Forward from the renderer side via history.go().
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.go(1)"));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FORWARD_BACK |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());
  }

  // Replace history.state to "foo".
  ReplaceState(root, "foo");
  EXPECT_EQ("foo", EvalJs(root, "history.state"));
  NavigationEntryImpl* previous_entry = controller.GetLastCommittedEntry();

  {
    // Reload the tab from the browser side.
    FrameNavigateParamsCapturer capturer(root);
    controller.Reload(ReloadType::NORMAL, false);
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_RELOAD));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());

    // We reused the last committed entry for this navigation.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(root, "history.state"));

    previous_entry = controller.GetLastCommittedEntry();
  }

  {
    // Reload the frame from the browser side.
    FrameNavigateParamsCapturer capturer(root);
    shell()->web_contents()->GetPrimaryMainFrame()->Reload();
    capturer.Wait();
    // We're classifying this as EXISTING_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());

    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_RELOAD));

    // We reused the last committed entry for this navigation.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(root, "history.state"));

    previous_entry = controller.GetLastCommittedEntry();
  }

  {
    // Reload from the renderer side.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "location.reload()"));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
                                  ui::PAGE_TRANSITION_CLIENT_REDIRECT)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());

    // We reused the last committed entry for this navigation.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(root, "history.state"));

    previous_entry = controller.GetLastCommittedEntry();
  }

  // Set up an URLLoaderInterceptor which will cause all navigations to fail.
  auto url_loader_interceptor = std::make_unique<URLLoaderInterceptor>(
      base::BindRepeating([](URLLoaderInterceptor::RequestParams* params) {
        network::URLLoaderCompletionStatus status;
        status.error_code = net::ERR_NOT_IMPLEMENTED;
        params->client->OnComplete(status);
        return true;
      }));

  // Reload the tab (browser-initiated), but this time we hit a network error
  // and end up in an error page.
  {
    TestNavigationObserver reload_observer(shell()->web_contents());
    FrameNavigateParamsCapturer capturer(root);
    shell()->Reload();
    capturer.Wait();
    EXPECT_FALSE(reload_observer.last_navigation_succeeded());

    // We're classifying this as EXISTING_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_RELOAD));

    // We reused the last committed entry for this navigation.
    // TODO(https://crbug.com/1188956): This should replace the last committed
    // entry instead.
    EXPECT_FALSE(capturer.did_replace_entry());
    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(previous_entry, entry);
    EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
    previous_entry = entry;
  }

  // Reload the tab and hit the same network error again.
  {
    TestNavigationObserver reload_observer(shell()->web_contents());
    FrameNavigateParamsCapturer capturer(root);
    shell()->Reload();
    capturer.Wait();
    EXPECT_FALSE(reload_observer.last_navigation_succeeded());

    // We're classifying this as EXISTING_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_RELOAD));

    // We reused the last committed entry for this navigation.
    // TODO(https://crbug.com/1188956): This should replace the last committed
    // entry instead.
    EXPECT_FALSE(capturer.did_replace_entry());
    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(previous_entry, entry);
    EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
    previous_entry = entry;
  }

  // Reset the  URLLoaderInterceptor so future navigations will succeed.
  url_loader_interceptor.reset();

  {
    // Reload the tab successfully after a failed navigation.
    TestNavigationObserver reload_observer(shell()->web_contents());
    FrameNavigateParamsCapturer capturer(root);
    shell()->Reload();
    capturer.Wait();
    EXPECT_TRUE(reload_observer.last_navigation_succeeded());

    // We're classifying this as NEW_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_RELOAD));

    // We replaced the last committed entry with a new entry for this
    // navigation.
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());

    // We lost the history.state value from before the failed navigation.
    EXPECT_EQ(nullptr, EvalJs(root, "history.state"));
    previous_entry = controller.GetLastCommittedEntry();
  }

  {
    // location.replace().
    FrameNavigateParamsCapturer capturer(root);
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_1.html"));
    std::string script = JsReplace("location.replace($1);", frame_url);
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
                                  ui::PAGE_TRANSITION_CLIENT_REDIRECT)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_FALSE(capturer.is_same_document());

    // We replaced the last committed entry with a new entry for this
    // navigation.
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());

    previous_entry = controller.GetLastCommittedEntry();
  }

  // Now, various same document navigations.

  scoped_refptr<FrameNavigationEntry> previous_root_entry =
      previous_entry->root_node()->frame_entry.get();

  {
    // Same-document location.replace().
    FrameNavigateParamsCapturer capturer(root);
    std::string script = "location.replace('#foo')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
                                  ui::PAGE_TRANSITION_CLIENT_REDIRECT)));
    // Different from cross-document replacement which would be classified as
    // NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, same-document replacement is
    // classified as NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_TRUE(capturer.is_same_document());

    // The FrameNavigationEntry and NavigationEntry are reused.
    EXPECT_EQ(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    previous_entry = controller.GetLastCommittedEntry();
    previous_root_entry = previous_entry->root_node()->frame_entry.get();
  }

  {
    // history.replaceState().
    FrameNavigateParamsCapturer capturer(root);
    std::string script =
        "history.replaceState({}, 'page 2', 'simple_page_2.html')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
                                  ui::PAGE_TRANSITION_CLIENT_REDIRECT)));
    // Different from history.pushState() which would be classified as
    // NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, replaceState() will be classified
    // as NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_TRUE(capturer.is_same_document());

    // The FrameNavigationEntry and NavigationEntry are reused.
    EXPECT_EQ(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());

    previous_entry = controller.GetLastCommittedEntry();
  }

  // Back and forward across a fragment navigation.

  GURL url_links(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_links.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url_links));
  std::string script = "document.getElementById('fraglink').click()";
  EXPECT_TRUE(ExecJs(root, script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  {
    // Back.
    FrameNavigateParamsCapturer capturer(root);
    shell()->web_contents()->GetController().GoBack();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FORWARD_BACK |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());
  }

  {
    // Forward.
    FrameNavigateParamsCapturer capturer(root);
    controller.GoForward();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
                                  ui::PAGE_TRANSITION_FORWARD_BACK)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());
  }

  // Back and forward across a pushState-created navigation.

  EXPECT_TRUE(NavigateToURL(shell(), url1));
  script = "history.pushState({}, 'page 2', 'simple_page_2.html')";
  EXPECT_TRUE(ExecJs(root, script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  {
    // Back.
    FrameNavigateParamsCapturer capturer(root);
    controller.GoBack();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FORWARD_BACK |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());
  }

  {
    // Forward.
    FrameNavigateParamsCapturer capturer(root);
    controller.GoForward();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
                                  ui::PAGE_TRANSITION_FORWARD_BACK)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());
  }
}

// Verify that navigations to the same URL are correctly classified as
// EXISTING_ENTRY (if it becomes a reload) or NEW_ENTRY (if it becomes a
// replacement navigation).
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NavigationTypeClassification_ExistingEntrySameURL) {
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  EXPECT_EQ(1, controller.GetEntryCount());
  // Replace history.state to "foo".
  ReplaceState(root, "foo");
  EXPECT_EQ("foo", EvalJs(root, "history.state"));

  NavigationEntryImpl* previous_entry = controller.GetLastCommittedEntry();

  {
    // Navigate to the same URL (browser-initiated).
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(NavigateToURL(shell(), url1));
    capturer.Wait();
    // The navigation got converted into a reload, and we're classifying this as
    // EXISTING_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());

    // Ensure the pending entry was cleared after commit.
    EXPECT_FALSE(shell()->web_contents()->GetController().GetPendingEntry());

    // We reuse the last committed entry for this navigation.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(1, controller.GetEntryCount());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(root, "history.state"));

    previous_entry = controller.GetLastCommittedEntry();
  }

  {
    // Navigate to the same URL (renderer-initiated).
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(NavigateToURLFromRenderer(root, url1));
    capturer.Wait();
    // We're classifying this as NEW_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());

    // The navigation replaced the previously committed entry with a new entry.
    // This differs than the browser-initiated case's behavior, but it's OK.
    // The renderer-initiated navigation follows the spec  at
    // https://html.spec.whatwg.org/C/#navigating-across-documents:hh-replace-3,
    // while the browser-initiated version got converted into a reload.
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(1, controller.GetEntryCount());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(root, "history.state"));

    previous_entry = controller.GetLastCommittedEntry();
  }

  {
    //  Navigate to the same URL (renderer-initiated) with location.replace.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, JsReplace("location.replace($1);", url1)));
    capturer.Wait();
    // We're classifying this as NEW_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());

    // The navigation replaced the previously committed entry with a new entry.
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(1, controller.GetEntryCount());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(root, "history.state"));

    previous_entry = controller.GetLastCommittedEntry();
  }

  auto url_loader_interceptor = std::make_unique<URLLoaderInterceptor>(
      base::BindRepeating([](URLLoaderInterceptor::RequestParams* params) {
        network::URLLoaderCompletionStatus status;
        status.error_code = net::ERR_NOT_IMPLEMENTED;
        params->client->OnComplete(status);
        return true;
      }));

  {
    // Navigate to the same URL (renderer-initiated), but this time we hit a
    // network error and end up in an error page. This will result in
    // replacement as it's a same-url navigation.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_FALSE(NavigateToURLFromRenderer(shell(), url1));
    capturer.Wait();
    // We're classifying this as NEW_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());

    // The navigation replaced the previously committed entry.
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(1, controller.GetEntryCount());

    previous_entry = controller.GetLastCommittedEntry();
  }

  {
    // Navigate to the same URL (through explicit reload), and hit a network
    // error again. This will reload the previous entry.
    FrameNavigateParamsCapturer capturer(root);
    shell()->Reload();
    capturer.Wait();
    // We're classifying this as EXISTING_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());

    // The navigation reused the previously committed error page entry.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(1, controller.GetEntryCount());

    previous_entry = controller.GetLastCommittedEntry();
  }

  {
    // Navigate to the same URL (browser-initiated), and hit a network error
    // again. The navigation initially got converted into a reload by the
    // browser but since it failed and doesn't have a valid PageState, it does
    // replacement instead.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_FALSE(NavigateToURL(shell(), url1));
    capturer.Wait();
    // We're classifying this as NEW_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());

    // The navigation replaced the previously committed entry.
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(1, controller.GetEntryCount());

    previous_entry = controller.GetLastCommittedEntry();
  }
  // Stop failing future navigations.
  url_loader_interceptor.reset();

  {
    // Navigate successfully to the same URL (browser-initiated) after a failed
    // navigation.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(NavigateToURL(shell(), url1));
    capturer.Wait();
    // We're classifying this as NEW_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());

    // The navigation added a new entry.
    // TODO(https://crbug.com/1188956): This should replace the last committed
    // entry instead.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(2, controller.GetEntryCount());

    // We lost the history.state value from before the failed navigation.
    EXPECT_EQ(nullptr, EvalJs(root, "history.state"));
    previous_entry = controller.GetLastCommittedEntry();
  }

  // Replace history.state to "foo".
  ReplaceState(root, "foo");
  EXPECT_EQ("foo", EvalJs(root, "history.state"));
  previous_entry = controller.GetLastCommittedEntry();

  {
    // Navigate to the same URL (browser-initiated) with LoadURLParams, setting
    // should_replace_current_entry to true. The browser should not convert this
    // navigation to do a reload, and should continue to do replacement.
    FrameNavigateParamsCapturer capturer(root);
    NavigationController::LoadURLParams params(url1);
    params.transition_type = ui::PageTransitionFromInt(
        ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
    params.should_replace_current_entry = true;
    contents()->GetController().LoadURLWithParams(params);
    capturer.Wait();
    // We're classifying this as NEW_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());

    // The navigation replaced the previously committed entry with a new entry.
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(2, controller.GetEntryCount());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(root, "history.state"));
    previous_entry = controller.GetLastCommittedEntry();
  }

  {
    // Navigate to the same URL (browser-initiated) with LoadURLParams, setting
    // should_replace_current_entry to true, but hit a network error. The
    // browser will convert this navigation to do a reload, but ended up doing
    // replacement instead.
    url_loader_interceptor = std::make_unique<URLLoaderInterceptor>(
        base::BindRepeating([](URLLoaderInterceptor::RequestParams* params) {
          network::URLLoaderCompletionStatus status;
          status.error_code = net::ERR_NOT_IMPLEMENTED;
          params->client->OnComplete(status);
          return true;
        }));

    FrameNavigateParamsCapturer capturer(root);
    NavigationController::LoadURLParams params(url1);
    params.transition_type = ui::PageTransitionFromInt(
        ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
    params.should_replace_current_entry = true;
    contents()->GetController().LoadURLWithParams(params);
    capturer.Wait();

    // We're classifying this as NEW_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());

    // The navigation replaced the previously committed entry with a new entry
    // because the navigation resulted in an error page.
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(2, controller.GetEntryCount());

    url_loader_interceptor.reset();
  }
}

// Verify that navigations to the same WebUI URL are correctly classified as
// EXISTING_ENTRY (it becomes a reload).
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    NavigationTypeClassification_ExistingEntrySameURL_WebUI) {
  // Navigate to a WebUI page.
  GURL web_ui_url(std::string(kChromeUIScheme) + "://" +
                  std::string(kChromeUIGpuHost));
  EXPECT_TRUE(NavigateToURL(shell(), web_ui_url));

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_EQ(1, controller.GetEntryCount());

  NavigationEntryImpl* previous_entry = controller.GetLastCommittedEntry();

  {
    // Navigate to the same Web UI URL (browser-initiated).
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(NavigateToURL(shell(), web_ui_url));
    capturer.Wait();
    // The navigation got converted into a reload, and we're classifying this as
    // EXISTING_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());

    // Ensure the pending entry was cleared after commit.
    EXPECT_FALSE(shell()->web_contents()->GetController().GetPendingEntry());

    // We reuse the last committed entry for this navigation.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(1, controller.GetEntryCount());

    previous_entry = controller.GetLastCommittedEntry();
  }

  {
    // Navigate to the same WebUI URL (renderer-initiated). This will go through
    // the "browser-initiated" path in the browser (OpenURL instead of
    // BeginNavigation). This would behave like a normal renderer-initiated
    // navigation if https://crbug.com/883549 were resolved.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(contents(), JsReplace("location.href = $1", web_ui_url),
                       EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */));
    capturer.Wait();
    // The navigation got converted into a reload, and we're classifying this as
    // EXISTING_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());

    // We reuse the last committed entry for this navigation.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(1, controller.GetEntryCount());
  }
}

// Verify that navigations to the same URL are correctly classified as
// EXISTING_ENTRY even after redirects.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    NavigationTypeClassification_ExistingEntrySameURLRedirect) {
  GURL url1(embedded_test_server()->GetURL("/title1.html"));
  GURL url2(embedded_test_server()->GetURL("/title2.html"));
  GURL redirect_to_url2(
      embedded_test_server()->GetURL("/server-redirect?" + url2.spec()));
  // Navigate to |url_1|.
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  EXPECT_EQ(1, controller.GetEntryCount());
  FrameTreeNode* root =
      static_cast<WebContentsImpl*>(contents())->GetPrimaryFrameTree().root();

  // Change the current URL to |redirect_to_url2| with replaceState, which
  // will stay in the same document and won't trigger an actual load to
  // |redirect_to_url2|.
  ReplaceState(root, "", redirect_to_url2);
  NavigationEntryImpl* previous_entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(redirect_to_url2, previous_entry->GetURL());

  {
    // 1) Do a browser-initiated navigation to |redirect_to_url2|.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(NavigateToURL(shell(), redirect_to_url2, url2));
    capturer.Wait();

    // Since it started out as a browser-initiated same-URL navigation, it got
    // converted to a reload and classified as
    // NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY and reuses the previous entry,
    // even though it ended up at a different URL than before.
    // TODO(https://crbug.com/1247185): Perhaps this should be classified as
    // NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY with replacement instead, to be
    // consistent with reloads that redirected cross-site.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_FALSE(capturer.did_replace_entry());

    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(url2, controller.GetLastCommittedEntry()->GetURL());
    EXPECT_EQ(1, controller.GetEntryCount());
  }

  // Change the current URL to |redirect_to_url2| with replaceState again.
  ReplaceState(root, "", redirect_to_url2);
  previous_entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(redirect_to_url2, previous_entry->GetURL());
  EXPECT_EQ(1, controller.GetEntryCount());

  {
    // 2) Do a renderer-initiated navigation to |redirect_to_url2|.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(NavigateToURLFromRenderer(root, redirect_to_url2, url2));
    capturer.Wait();

    // Since it started out as a renderer-initiated same-URL navigation, it got
    // converted to a replacement navigation, so it's classified as
    // NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());

    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(url2, controller.GetLastCommittedEntry()->GetURL());
    EXPECT_EQ(1, controller.GetEntryCount());
  }

  // Change the current URL to |redirect_to_url2| with replaceState again.
  ReplaceState(root, "", redirect_to_url2);
  previous_entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(redirect_to_url2, previous_entry->GetURL());
  EXPECT_EQ(1, controller.GetEntryCount());

  {
    // 3) Do a form submission to |redirect_to_url2|.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(contents(),
                       JsReplace(R"(var form = document.createElement('form');
                                 form.method = 'POST';
                                 form.action = $1;
                                 document.body.appendChild(form);
                                 form.submit();)",
                                 redirect_to_url2)));
    capturer.Wait();

    // It started out as a same-URL navigation, but since it was a POST
    // navigation, it didn't get converted to replacement.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_FALSE(capturer.did_replace_entry());

    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(url2, controller.GetLastCommittedEntry()->GetURL());
    EXPECT_EQ(2, controller.GetEntryCount());

    // The POST data should not be saved in the final NavigationEntry.
    EXPECT_FALSE(controller.GetLastCommittedEntry()->GetHasPostData());
  }
}

// Verify that navigations to the same URL that has a fragment part (#foo) are
// correctly classified as EXISTING_ENTRY.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    NavigationTypeClassification_ExistingEntrySameURLWithFragment) {
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html#foo"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  // Replace history.state to "foo".
  ReplaceState(root, "foo");
  EXPECT_EQ("foo", EvalJs(root, "history.state"));

  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntryImpl* previous_entry = controller.GetLastCommittedEntry();

  {
    // Navigate to the same URL (browser-initiated).
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(NavigateToURL(shell(), url1));
    capturer.Wait();
    // We're classifying this as EXISTING_ENTRY because the navigation got
    // converted into a reload.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());

    // Since we did a reload, it's not classified as a same-document navigation.
    EXPECT_FALSE(capturer.is_same_document());

    // We reuse the last committed entry for this navigation.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(1, controller.GetEntryCount());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(root, "history.state"));

    previous_entry = controller.GetLastCommittedEntry();
  }

  {
    // Navigate to the same URL (renderer-initiated).
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(NavigateToURLFromRenderer(root, url1));
    capturer.Wait();

    // We did a same-document navigation.
    EXPECT_TRUE(capturer.is_same_document());

    // We're reusing the previous entry and classifying this as EXISTING_ENTRY
    // with replacement.
    // TODO(rakina): did_replace_entry should be false since we're not actually
    // doing any replacement.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(1, controller.GetEntryCount());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(root, "history.state"));
  }
}

// Verify that reloading a page with url anchor scrolls to correct position.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, ReloadWithUrlAnchor) {
  GURL url(embedded_test_server()->GetURL(
      "/navigation_controller/reload-with-url-anchor.html#center-element"));
  EXPECT_TRUE(NavigateToURL(shell(), url));

  double window_scroll_y = EvalJs(shell(), "window.scrollY").ExtractDouble();

  // TODO(bokan): The floor hack below can go
  // away once FractionalScrolLOffsets ships. The reason it's required is that,
  // at certain device scale factors, the given CSS pixel scroll value may land
  // between physical pixels. Without the feature Blink will truncate to the
  // nearest physical pixel so the expectation must account for that. When the
  // feature is enabled, Blink stores the fractional offset so the truncation
  // is unnecessary. https://crbug.com/414283.
  bool fractional_scroll_offsets_enabled = IsFractionalScrollOffsetsEnabled();

  // The 'center-element' y-position is 2000px. 2000px is an arbitrary value.
  double expected_window_scroll_y = 2000;
  if (!fractional_scroll_offsets_enabled) {
    float device_scale_factor = shell()
                                    ->web_contents()
                                    ->GetRenderWidgetHostView()
                                    ->GetDeviceScaleFactor();
    expected_window_scroll_y =
        floor(device_scale_factor * expected_window_scroll_y) /
        device_scale_factor;
  }
  EXPECT_FLOAT_EQ(expected_window_scroll_y, window_scroll_y);

  // Reload.
  ReloadBlockUntilNavigationsComplete(shell(), 1);

  window_scroll_y = EvalJs(shell(), "window.scrollY").ExtractDouble();
  EXPECT_FLOAT_EQ(expected_window_scroll_y, window_scroll_y);
}

// Verify that reloading a page with url anchor and scroll scrolls to correct
// position.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ReloadWithUrlAnchorAndScroll) {
  GURL url(embedded_test_server()->GetURL(
      "/navigation_controller/reload-with-url-anchor.html#center-element"));
  EXPECT_TRUE(NavigateToURL(shell(), url));

  // The 'center-element' y-position is 2000px. This script scrolls the view
  // 100px below this element. 2000px and 100px are arbitrary values.
  std::string script_scroll_down = "window.scroll(0, 2100)";
  EXPECT_TRUE(ExecJs(shell(), script_scroll_down));

  double window_scroll_y = EvalJs(shell(), "window.scrollY").ExtractDouble();

  // TODO(bokan): The floor hack below can go
  // away once FractionalScrolLOffsets ships. The reason it's required is that,
  // at certain device scale factors, the given CSS pixel scroll value may land
  // between physical pixels. Without the feature Blink will truncate to the
  // nearest physical pixel so the expectation must account for that. When the
  // feature is enabled, Blink stores the fractional offset so the truncation
  // is unnecessary. https://crbug.com/414283.
  bool fractional_scroll_offsets_enabled = IsFractionalScrollOffsetsEnabled();

  double expected_window_scroll_y = 2100;
  if (!fractional_scroll_offsets_enabled) {
    float device_scale_factor = shell()
                                    ->web_contents()
                                    ->GetRenderWidgetHostView()
                                    ->GetDeviceScaleFactor();
    expected_window_scroll_y =
        floor(device_scale_factor * expected_window_scroll_y) /
        device_scale_factor;
  }
  EXPECT_FLOAT_EQ(expected_window_scroll_y, window_scroll_y);

  // Reload.
  ReloadBlockUntilNavigationsComplete(shell(), 1);

  window_scroll_y = EvalJs(shell(), "window.scrollY").ExtractDouble();
  EXPECT_FLOAT_EQ(expected_window_scroll_y, window_scroll_y);
}

// Verify that empty GURL navigations are not classified as EXISTING_ENTRY.
// See https://crbug.com/534980.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NavigationTypeClassification_EmptyGURL) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  EXPECT_EQ(1, controller.GetEntryCount());
  {
    // Load an (invalid) empty GURL.  Blink will treat this as an inert commit,
    // but we don't want it to show up as EXISTING_ENTRY.
    FrameNavigateParamsCapturer capturer(root);
    NavigateFrameToURL(root, GURL());
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_LINK));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());

    // The navigation should add a new entry to the session history, and not
    // do any entry replacement.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(2, controller.GetEntryCount());
  }
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RendererInitiatedNavigationToEmptyGURLFails) {
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Trying to navigate to an empty URL from the renderer will fail.
  EXPECT_FALSE(NavigateToURLFromRenderer(root, GURL()));
}

// Verify that navigations for NAVIGATION_TYPE_NEW_SUBFRAME and
// NAVIGATION_TYPE_AUTO_SUBFRAME are properly classified.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NavigationTypeClassification_NewAndAutoSubframe) {
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  // It is safe to obtain the root frame tree node here, as it doesn't change.
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  ASSERT_EQ(1U, root->child_count());
  ASSERT_NE(nullptr, root->child_at(0));

  {
    // Initial load.
    LoadCommittedCapturer capturer(root->child_at(0));
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_1.html"));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  {
    // Simple load.
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_2.html"));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
  }

  {
    // Back.
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    shell()->web_contents()->GetController().GoBack();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
  }

  {
    // Forward.
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    shell()->web_contents()->GetController().GoForward();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
  }

  {
    // Simple load.
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/page_with_links.html"));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
  }

  {
    // Load via a fragment link click.
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    std::string script = "document.getElementById('fraglink').click()";
    EXPECT_TRUE(ExecJs(root->child_at(0), script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
  }

  {
    // location.assign().
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_1.html"));
    std::string script = JsReplace("location.assign($1);", frame_url);
    EXPECT_TRUE(ExecJs(root->child_at(0), script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
  }

  {
    // location.replace().
    LoadCommittedCapturer capturer(root->child_at(0));
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_2.html"));
    std::string script = JsReplace("location.replace($1);", frame_url);
    EXPECT_TRUE(ExecJs(root->child_at(0), script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  {
    // history.pushState().
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    std::string script =
        "history.pushState({}, 'page 1', 'simple_page_1.html')";
    EXPECT_TRUE(ExecJs(root->child_at(0), script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
  }

  {
    // history.replaceState().
    LoadCommittedCapturer capturer(root->child_at(0));
    std::string script =
        "history.replaceState({}, 'page 2', 'simple_page_2.html')";
    EXPECT_TRUE(ExecJs(root->child_at(0), script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  {
    // Reload.
    LoadCommittedCapturer capturer(root->child_at(0));
    EXPECT_TRUE(ExecJs(root->child_at(0), "location.reload()"));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  {
    // Create an iframe.
    LoadCommittedCapturer capturer(shell()->web_contents());
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_1.html"));
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, frame_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }
}

// Captures LoadCommittedDetails to compare against expectations.
class LoadCommittedDetailsObserver : public WebContentsObserver {
 public:
  explicit LoadCommittedDetailsObserver(WebContents* web_contents)
      : WebContentsObserver(web_contents) {}

  void Wait() { loop_.Run(); }

  const LoadCommittedDetails& load_details() { return load_details_; }

 private:
  void NavigationEntryCommitted(
      const LoadCommittedDetails& load_details) override {
    load_details_ = load_details;
    loop_.Quit();
  }

  LoadCommittedDetails load_details_;

  base::RunLoop loop_;
};

// Tests for navigations that happen after initial empty document loads on an
// iframe/opened window. This class is parameterized by both RenderDocumentHost
// mode and by whether it would do renderer vs browser initiated navigations.
class InitialEmptyDocNavigationControllerBrowserTest
    : public NavigationControllerBrowserTestBase,
      public testing::WithParamInterface<
          std::tuple<std::string, bool /* renderer_initiated */>> {
 public:
  InitialEmptyDocNavigationControllerBrowserTest() {
    InitAndEnableRenderDocumentFeature(&feature_list_for_render_document_,
                                       std::get<0>(GetParam()));
  }

  // Provides meaningful param names instead of /0, /1, ...
  static std::string DescribeParams(
      const testing::TestParamInfo<ParamType>& info) {
    auto [render_document_level, renderer_initiated] = info.param;
    return base::StringPrintf(
        "%s_%s",
        GetRenderDocumentLevelNameForTestParams(render_document_level).c_str(),
        renderer_initiated ? "RendererInitiated" : "BrowserInitiated");
  }

 protected:
  bool renderer_initiated() { return std::get<1>(GetParam()); }

  // Navigates |node| to |url| then checks if its navigation type is
  // |navigation_type| and whether other related properties are consistent with
  // the type. Whether the navigation is renderer-initiated or not depends on
  // the renderer vs browser initiated parameter of this test class.
  void NavigateSubframeAndCheckNavigationType(WebContentsImpl* web_contents,
                                              FrameTreeNode* node,
                                              std::string frame_id,
                                              const GURL& url,
                                              NavigationType expected_type) {
    DCHECK(!node->IsMainFrame());
    FrameNavigateParamsCapturer capturer(node);
    if (renderer_initiated()) {
      EXPECT_TRUE(NavigateIframeToURL(web_contents, frame_id, url));
    } else {
      EXPECT_TRUE(NavigateFrameToURL(node, url));
    }
    capturer.Wait();

    EXPECT_EQ(expected_type, capturer.navigation_type());

    if (expected_type == NAVIGATION_TYPE_AUTO_SUBFRAME) {
      EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
          capturer.transition(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
      // |did_replace_entry| is true because the history item in the renderer
      // replaced the initial empty document.
      EXPECT_TRUE(capturer.did_replace_entry());

    } else {
      EXPECT_EQ(expected_type, NAVIGATION_TYPE_NEW_SUBFRAME);
      EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
          capturer.transition(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
      EXPECT_FALSE(capturer.did_replace_entry());
    }
  }

  // Navigates |web_contents| to |url| then check its navigation type and
  // whether other related properties are consistent with the type. Whether the
  // navigation is renderer-initiated or not depends on the renderer vs browser
  // initiated parameter of this test class.
  void NavigateWindowAndCheck(WebContentsImpl* web_contents,
                              const GURL& url,
                              bool wait_for_previous_navigations = true,
                              bool expect_same_document = false,
                              NavigationType expected_navigation_type =
                                  NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY,
                              bool expect_replace = true) {
    FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
    LoadCommittedDetailsObserver load_details_observer(web_contents);
    FrameNavigateParamsCapturer capturer(root);
    if (renderer_initiated()) {
      EXPECT_TRUE(NavigateToURLFromRenderer(web_contents, url));
    } else {
      // Do a browser-initiated navigation. In cases where there's a previous
      // navigation that hasn't finished and won't finish (e.g. navigations to
      // /hung), we can't use NavigateToURL(), because it will wait for the
      // previous navigation to finish first. So, use LoadURLWithParams()
      // directly in those cases.
      if (!wait_for_previous_navigations) {
        NavigationController::LoadURLParams params(url);
        params.transition_type = ui::PageTransitionFromInt(
            ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
        web_contents->GetController().LoadURLWithParams(params);
      } else {
        // Otherwise, just use NavigateToURL().
        EXPECT_TRUE(NavigateToURL(web_contents, url));
      }
    }
    capturer.Wait();
    load_details_observer.Wait();

    EXPECT_EQ(expected_navigation_type, capturer.navigation_type());
    EXPECT_EQ(expect_replace, capturer.did_replace_entry());

    // Check both NavigationHandle and LoadCommittedDetails for whether this was
    // considered same-document, as these have diverged in the past.
    // See https://crbug.com/1193134.
    EXPECT_EQ(expect_same_document, capturer.is_same_document());
    EXPECT_EQ(expect_same_document,
              load_details_observer.load_details().is_same_document);
  }

 private:
  base::test::ScopedFeatureList feature_list_for_render_document_;
};

// Test various navigation cases on newly-created subframes that have only
// loaded the initial empty document (but might have done other navigations that
// stay in the initial empty document), to see if the initial empty documents
// get replaced/not replaced.
// TODO(https://crbug.com/1215096): Most of these cases are not actually running
// on the initial empty document because they have committed the synchronous
// non-initial about:blank document. Update these tests or remove the
// synchronous navigation entirely.
IN_PROC_BROWSER_TEST_P(InitialEmptyDocNavigationControllerBrowserTest,
                       NavigateNewSubframe) {
  GURL url_1(embedded_test_server()->GetURL("/title1.html"));
  GURL url_2(embedded_test_server()->GetURL("/title2.html"));
  GURL no_commit_url(embedded_test_server()->GetURL("/page204.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url_1));

  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  EXPECT_EQ(1, controller.GetEntryCount());

  int subframe_index = 0;
  int expected_entry_count = 1;

  // 1) Navigate to |url_2| on a new subframe that hasn't done any navigation.
  {
    SCOPED_TRACE(testing::Message() << " Testing case 1.");

    // Create the "child1" subframe without navigating it.
    CreateSubframe(contents(), "child1", GURL(),
                   false /* wait_for_navigation */);

    // The new subframe should be on the initial empty document.
    FrameTreeNode* new_subframe = root->child_at(subframe_index);
    EXPECT_TRUE(new_subframe->is_on_initial_empty_document());

    // Do a navigation on the "child1" subframe to |url_2|.
    // The navigation is still classified as "auto", so we didn't append a new
    // NavigationEntry, and instead updated the current NavigationEntry.
    NavigateSubframeAndCheckNavigationType(contents(), new_subframe, "child1",
                                           url_2,
                                           NAVIGATION_TYPE_AUTO_SUBFRAME);
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());
  }

  // 2) Navigate to |url_2| on a new subframe that has done a navigation to
  // about:blank and a same-document navigation to about:blank#foo.
  {
    SCOPED_TRACE(testing::Message() << " Testing case 2.");

    // Create the "child2" subframe with src set to about:blank, navigating it
    // there.
    CreateSubframe(contents(), "child2", GURL("about:blank"),
                   true /* wait_for_navigation */);
    subframe_index++;
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());

    // The new subframe should be on the initial empty document.
    FrameTreeNode* new_subframe = root->child_at(subframe_index);
    EXPECT_TRUE(new_subframe->is_on_initial_empty_document());

    // Do a navigation on the "child2" subframe to about:blank#foo, creating a
    // same-document navigation. The navigation will do a replacement and get
    // classified as "auto", so we won't append a new NavigationEntry, and
    // instead update the current NavigationEntry.
    NavigateSubframeAndCheckNavigationType(contents(), new_subframe, "child2",
                                           GURL("about:blank#foo"),
                                           NAVIGATION_TYPE_AUTO_SUBFRAME);
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());

    // The subframe should still be on the initial empty document.
    EXPECT_TRUE(new_subframe->is_on_initial_empty_document());

    // Do a navigation on the "child2" subframe to |url_2|.
    NavigateSubframeAndCheckNavigationType(contents(), new_subframe, "child2",
                                           url_2,
                                           NAVIGATION_TYPE_AUTO_SUBFRAME);
    // The navigation is still classified as "auto", so we didn't append a new
    // NavigationEntry, and instead updated the current NavigationEntry.
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());

    // The subframe should no longer be on the initial empty document.
    EXPECT_FALSE(new_subframe->is_on_initial_empty_document());
  }

  // 3) Navigate to |url_2| on a new subframe that has done a navigation to a
  // data: URL.
  {
    SCOPED_TRACE(testing::Message() << " Testing case 3.");

    // Create the "child3" subframe with src set to a data: URL, navigating it
    // there.
    CreateSubframe(contents(), "child3", GURL("data:text/html,foo"),
                   true /* wait_for_navigation */);
    subframe_index++;
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());

    // The new subframe should not be on the initial empty document.
    FrameTreeNode* new_subframe = root->child_at(subframe_index);
    EXPECT_FALSE(new_subframe->is_on_initial_empty_document());

    // Do a navigation on the "child3" subframe to |url_2|.
    // The navigation is classified as a new navigation, and appended a new
    // NavigationEntry.
    NavigateSubframeAndCheckNavigationType(contents(), new_subframe, "child3",
                                           url_2, NAVIGATION_TYPE_NEW_SUBFRAME);
    expected_entry_count++;
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());
  }

  // 4) Navigate to |url_2| on a new subframe that has started a navigation to
  // a URL that never committed.
  // TODO(https://crbug.com/1311616): The browser-initiated case is flaky so we
  // only test this for the renderer-initiated case. Figure out how to test the
  // browser-initiated case in a non-flaky way.
  if (renderer_initiated()) {
    SCOPED_TRACE(testing::Message() << " Testing case 4.");

    // Create the "child4" subframe with src set to a URL that never commits.
    CreateSubframe(contents(), "child4", no_commit_url,
                   false /* wait_for_navigation */);
    subframe_index++;
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());

    // The new subframe should be on the initial empty document.
    FrameTreeNode* new_subframe = root->child_at(subframe_index);
    EXPECT_TRUE(new_subframe->is_on_initial_empty_document());

    // Do a navigation on the "child4" subframe to |url_2|.
    // The navigation is still classified as "auto", so we didn't append a new
    // NavigationEntry, and instead updated the current NavigationEntry.
    NavigateSubframeAndCheckNavigationType(contents(), new_subframe, "child4",
                                           url_2,
                                           NAVIGATION_TYPE_AUTO_SUBFRAME);
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());

    // The subframe should no longer be on the initial empty document.
    EXPECT_FALSE(new_subframe->is_on_initial_empty_document());
  }

  // 5) Navigate to |url_2| on a new subframe that has done a document.open().
  {
    SCOPED_TRACE(testing::Message() << " Testing case 5.");

    // Create the "child5" subframe.
    CreateSubframe(contents(), "child5", GURL(),
                   false /* wait_for_navigation */);
    subframe_index++;
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());

    // The new subframe should be on the initial empty document.
    FrameTreeNode* new_subframe = root->child_at(subframe_index);
    EXPECT_EQ(GURL("about:blank"), new_subframe->current_url());
    EXPECT_TRUE(new_subframe->is_on_initial_empty_document());

    // Do a document.open() on it.
    EXPECT_TRUE(ExecJs(shell(), R"(
          var iframeDoc = document.getElementById("child5").contentDocument;
          iframeDoc.open();
          iframeDoc.write("foo");
          iframeDoc.close();
      )"));

    // Ensure the URL update from the document.open() above has finished before
    // continuing by waiting for a renderer round-trip to run this script.
    EXPECT_TRUE(ExecJs(new_subframe, "true"));

    // The document.open() changed the iframe's URL in the renderer to be the
    // same as the main frame's URL, but doesn't actually commit a navigation.
    EXPECT_EQ(GURL("about:blank"),
              new_subframe->current_frame_host()->GetLastCommittedURL());
    EXPECT_EQ(
        url_1,
        new_subframe->current_frame_host()->last_document_url_in_renderer());
    // The frame lost its "initial empty document" status.
    EXPECT_FALSE(new_subframe->is_on_initial_empty_document());

    // Do a navigation on the "child5" subframe to |url_2|.
    // The navigation is classified as a new navigation, and appended a new
    // NavigationEntry.
    NavigateSubframeAndCheckNavigationType(contents(), new_subframe, "child5",
                                           url_2, NAVIGATION_TYPE_NEW_SUBFRAME);
    expected_entry_count++;
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());

    // The frame should still be marked as no longer being on the initial empty
    // document.
    EXPECT_FALSE(new_subframe->is_on_initial_empty_document());
  }

  // 6) Navigate to |url_2| on a new subframe that has done a navigation to
  // a javascript: url that replaces the document.
  {
    SCOPED_TRACE(testing::Message() << " Testing case 6.");

    // Create the "child6" subframe and set it to a javascript: URL.
    RenderFrameHost* child6 =
        CreateSubframe(contents(), "child6", GURL("javascript:'foo'"),
                       false /* wait_for_navigation */);
    subframe_index++;
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());

    // The new subframe should be on the initial empty document.
    FrameTreeNode* new_subframe = root->child_at(subframe_index);
    EXPECT_TRUE(new_subframe->is_on_initial_empty_document());

    // The subframe that was just created loads javascript which generates
    // DidStopLoading(). RenderFrameHostImpl::DidStopLoading()
    // early outs in this case as `is_loading_` is false. Wait for this before
    // continuing. If we did not wait, then the navigation occurring after
    // this would set `is_loading_` to true, so that the DidStopLoading() for
    // about:blank would be processed, which this test does not expect
    // (NavigateSubframeAndCheckNavigationType() thinks the navigation failed).
    // TODO: ideally the renderer wouldn't send DidStopLoading() in this case.
    base::RunLoop run_loop;
    static_cast<RenderFrameHostImpl*>(child6)
        ->set_did_stop_loading_callback_for_testing(run_loop.QuitClosure());
    run_loop.Run();

    // Do a navigation on the "child6" subframe to |url_2|.
    // The navigation is still classified as "auto", so we didn't append a new
    // NavigationEntry, and instead updated the current NavigationEntry.
    NavigateSubframeAndCheckNavigationType(contents(), new_subframe, "child6",
                                           url_2,
                                           NAVIGATION_TYPE_AUTO_SUBFRAME);
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());

    // The subframe should no longer be on the initial empty document.
    EXPECT_FALSE(new_subframe->is_on_initial_empty_document());
  }

  // 7) Navigate to a non-initial/synchronous about:blank and then |url_2| on
  // a new subframe.
  {
    SCOPED_TRACE(testing::Message() << " Testing case 7.");

    // Create the "child7" subframe with src set to about:blank, navigating it
    // there.
    CreateSubframe(contents(), "child7", GURL("about:blank"),
                   true /* wait_for_navigation */);
    subframe_index++;
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());

    // The new subframe should be on the initial empty document.
    FrameTreeNode* new_subframe = root->child_at(subframe_index);
    EXPECT_TRUE(new_subframe->is_on_initial_empty_document());

    // Do a navigation on the "child7" subframe to about:blank?foo, creating a
    // new about:blank document that is neither the initial about:blank nor the
    // synchronously committed about:blank document, so it's not treated as the
    // initial empty document. The navigation will do a replacement and get
    // classified as "auto", so we won't append a new NavigationEntry, and
    // instead update the current NavigationEntry.
    NavigateSubframeAndCheckNavigationType(contents(), new_subframe, "child7",
                                           GURL("about:blank?foo"),
                                           NAVIGATION_TYPE_AUTO_SUBFRAME);
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());

    // The subframe should no longer be on the the initial empty document.
    EXPECT_FALSE(new_subframe->is_on_initial_empty_document());

    // Do a navigation on the "child7" subframe to |url_2|.
    NavigateSubframeAndCheckNavigationType(contents(), new_subframe, "child7",
                                           url_2, NAVIGATION_TYPE_NEW_SUBFRAME);
    // The navigation is classified as a new navigation, and appended a new
    // NavigationEntry.
    expected_entry_count++;
    EXPECT_EQ(expected_entry_count, controller.GetEntryCount());

    // The subframe is not on the initial empty document.
    EXPECT_FALSE(new_subframe->is_on_initial_empty_document());
  }
}

// Test various navigation cases on newly-created windows that have only loaded
// the initial empty document (but might have done other navigations that stay
// in the initial empty document), to see if the initial empty documents get
// replaced/not replaced.
// TODO(rakina): Most of these cases are not actually running on the initial
// empty document because they have committed the synchronous non-initial
// about:blank document. Update these tests or remove the synchronous
// navigation entirely.
IN_PROC_BROWSER_TEST_P(InitialEmptyDocNavigationControllerBrowserTest,
                       NavigateNewWindow) {
  GURL main_window_url(embedded_test_server()->GetURL("/title1.html"));
  GURL url_2(embedded_test_server()->GetURL("/title2.html"));
  GURL hung_url(embedded_test_server()->GetURL("/hung"));
  EXPECT_TRUE(NavigateToURL(shell(), main_window_url));
  EXPECT_TRUE(ExecJs(contents(), "var last_opened_window = null;"));

  // 1) Navigate to |url_2| on a new window that hasn't done any navigation.
  {
    SCOPED_TRACE(testing::Message() << " Testing case 1.");

    // Create a new blank window.
    Shell* new_shell = OpenBlankWindow(contents());
    WebContentsImpl* new_contents =
        static_cast<WebContentsImpl*>(new_shell->web_contents());
    NavigationControllerImpl& controller = new_contents->GetController();
    EXPECT_TRUE(!controller.GetLastCommittedEntry() ||
                controller.GetLastCommittedEntry()->IsInitialEntry());

    // Navigating the window to |url_2| will be classified as NEW_ENTRY and will
    // replace the previous entry if it exists.
    NavigateWindowAndCheck(new_contents, url_2);
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_FALSE(controller.GetLastCommittedEntry()->IsInitialEntry());
  }

  // 2) Navigate to about:blank on a new window that hasn't done any navigation.
  {
    SCOPED_TRACE(testing::Message() << " Testing case 2.");

    // Create a new blank window.
    Shell* new_shell = OpenBlankWindow(contents());
    WebContentsImpl* new_contents =
        static_cast<WebContentsImpl*>(new_shell->web_contents());
    NavigationControllerImpl& controller = new_contents->GetController();
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntry* last_entry = controller.GetLastCommittedEntry();

    // The window should be on the initial empty document and initial
    // NavigationEntry.
    FrameTreeNode* new_root = new_contents->GetPrimaryFrameTree().root();
    EXPECT_TRUE(new_root->is_on_initial_empty_document());
    EXPECT_TRUE(last_entry->IsInitialEntry());

    // Navigating the window to about:blank will be classified as NEW_ENTRY
    // and will replace the initial entry.
    NavigateWindowAndCheck(new_contents, GURL("about:blank"),
                           true /* wait_for_previous_navigations */,
                           false /* expect_same_document */,
                           NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY,
                           true /* expect_replace */);
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_NE(last_entry, controller.GetLastCommittedEntry());
    last_entry = controller.GetLastCommittedEntry();

    // The window is no longer on the initial empty document and initial
    // NavigationEntry.
    EXPECT_FALSE(new_root->is_on_initial_empty_document());
    EXPECT_FALSE(last_entry->IsInitialEntry());

    // Navigating the window to about:blank#foo will be classified as a same-
    // document NEW_ENTRY, and will add a new entry.
    NavigateWindowAndCheck(new_contents, GURL("about:blank#foo"),
                           true /* wait_for_previous_navigations */,
                           true /* expect_same_document */,
                           NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY,
                           false /* expect_replace */);
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_NE(last_entry, controller.GetLastCommittedEntry());

    // The window is still marked as no longer being on the initial empty
    // document.
    EXPECT_FALSE(new_root->is_on_initial_empty_document());
  }

  // 3) Navigate to about:blank#foo on a new window that hasn't done any
  // navigation.
  {
    SCOPED_TRACE(testing::Message() << " Testing case 3.");

    // Create a new blank window.
    Shell* new_shell = OpenBlankWindow(contents());
    WebContentsImpl* new_contents =
        static_cast<WebContentsImpl*>(new_shell->web_contents());
    NavigationControllerImpl& controller = new_contents->GetController();
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntry* last_entry = controller.GetLastCommittedEntry();

    // The window should be on the initial empty document and initial
    // NavigationEntry.
    FrameTreeNode* new_root = new_contents->GetPrimaryFrameTree().root();
    EXPECT_TRUE(new_root->is_on_initial_empty_document());
    EXPECT_TRUE(last_entry->IsInitialEntry());

    // Navigating the window to about:blank#foo will be classified as a same-
    // document replacement, and will be classified as EXISTING_ENTRY and modify
    // the last NavigationEntry for browser-initiated navigations.
    NavigateWindowAndCheck(new_contents, GURL("about:blank#foo"),
                           true /* wait_for_previous_navigations */,
                           true /* expect_same_document */,
                           NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY);
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_EQ(last_entry, controller.GetLastCommittedEntry());

    // The window should still be on the initial empty document and initial
    // NavigationEntry.
    EXPECT_TRUE(new_root->is_on_initial_empty_document());
    EXPECT_TRUE(last_entry->IsInitialEntry());
  }

  // 4) Navigate to |url_2| on a new window that initially loaded about:blank
  // and has done a same-document navigation to about:blank#foo.
  {
    SCOPED_TRACE(testing::Message() << " Testing case 4.");

    // Create a new window with URL set to about:blank.
    Shell* new_shell = OpenWindow(contents(), GURL("about:blank"));
    WebContentsImpl* new_contents =
        static_cast<WebContentsImpl*>(new_shell->web_contents());
    NavigationControllerImpl& controller = new_contents->GetController();
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntryImpl* last_entry = controller.GetLastCommittedEntry();

    // The window should be on the initial empty document and initial
    // NavigationEntry.
    FrameTreeNode* new_root = new_contents->GetPrimaryFrameTree().root();
    EXPECT_TRUE(new_root->is_on_initial_empty_document());
    EXPECT_TRUE(last_entry->IsInitialEntry());

    // Do a navigation on the window to about:blank#foo, creating a
    // same-document navigation.
    NavigateWindowAndCheck(new_contents, GURL("about:blank#foo"),
                           true /* wait_for_previous_navigations */,
                           true /* expect_same_document */,
                           NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
                           true /* expect_replace */);
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_EQ(last_entry, controller.GetLastCommittedEntry());
    // Check that we did a same-document navigation (the DSN stays the same).
    EXPECT_EQ(last_entry->GetMainFrameDocumentSequenceNumber(),
              controller.GetLastCommittedEntry()
                  ->GetMainFrameDocumentSequenceNumber());
    last_entry = controller.GetLastCommittedEntry();

    // The window should still be on the initial empty document and initial
    // NavigationEntry.
    EXPECT_TRUE(new_root->is_on_initial_empty_document());
    EXPECT_TRUE(last_entry->IsInitialEntry());

    // Navigating the window to |url_2| will be classified as NEW_ENTRY and will
    // replace the previous entry.
    NavigateWindowAndCheck(
        new_contents, url_2, true /* wait_for_previous_navigations */,
        false /* expect_same_document */, NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY,
        true /* expect_replace */);
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_NE(last_entry, controller.GetLastCommittedEntry());
    last_entry = controller.GetLastCommittedEntry();

    // The window is no longer on the initial empty document and initial
    // NavigationEntry.
    EXPECT_FALSE(new_root->is_on_initial_empty_document());
    EXPECT_FALSE(last_entry->IsInitialEntry());
  }

  // 5) Navigate to |url_2| on a new window that has started a navigation to
  // a URL that never committed.
  {
    SCOPED_TRACE(testing::Message() << " Testing case 5.");

    // Create a new window with URL set to a URL that never commits, which will
    // stay on the initial NavigationEntry.
    Shell* new_shell = OpenWindow(contents(), hung_url);
    WebContentsImpl* new_contents =
        static_cast<WebContentsImpl*>(new_shell->web_contents());
    NavigationControllerImpl& controller = new_contents->GetController();
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntryImpl* last_entry = controller.GetLastCommittedEntry();

    // The window should be on the initial empty document and initial
    // NavigationEntry.
    FrameTreeNode* new_root = new_contents->GetPrimaryFrameTree().root();
    EXPECT_TRUE(new_root->is_on_initial_empty_document());
    EXPECT_TRUE(last_entry->IsInitialEntry());

    // Navigate to |url_2|, and ensure that we won't wait for the |hung_url|
    // navigation to finish. The initial NavigationEntry will be replaced.
    NavigateWindowAndCheck(new_contents, url_2,
                           false /* wait_for_previous_navigations */);
    EXPECT_EQ(1, controller.GetEntryCount());
    last_entry = controller.GetLastCommittedEntry();

    // The window is no longer on the initial empty document and initial
    // NavigationEntry.
    EXPECT_FALSE(new_root->is_on_initial_empty_document());
    EXPECT_FALSE(last_entry->IsInitialEntry());
  }

  // 6) Navigate to |url_2| on a new window that has done a document.open().
  {
    SCOPED_TRACE(testing::Message() << " Testing case 6.");

    // Create a new blank window.
    Shell* new_shell = OpenBlankWindow(contents());
    WebContentsImpl* new_contents =
        static_cast<WebContentsImpl*>(new_shell->web_contents());
    NavigationControllerImpl& controller = new_contents->GetController();
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntryImpl* last_entry = controller.GetLastCommittedEntry();

    // The window should be on the initial empty document and initial
    // NavigationEntry.
    FrameTreeNode* new_root = new_contents->GetPrimaryFrameTree().root();
    EXPECT_TRUE(new_root->is_on_initial_empty_document());
    EXPECT_TRUE(last_entry->IsInitialEntry());

    // Do a document.open() on the blank window.
    TestNavigationObserver nav_observer(new_contents);
    EXPECT_TRUE(ExecJs(contents(), R"(
      last_opened_window.document.open();
          last_opened_window.document.write("foo");
          last_opened_window.document.close();
    )"));

    // Ensure the URL update from the document.open() above has finished before
    // continuing by waiting for a renderer round-trip to run this script.
    EXPECT_TRUE(ExecJs(new_contents, "true"));

    // The document.open() changed the window's URL in the renderer to be the
    // same as the main tab's URL, but doesn't actually commit a navigation.
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_EQ(GURL("about:blank"),
              new_contents->GetPrimaryMainFrame()->GetLastCommittedURL());
    EXPECT_EQ(
        main_window_url,
        new_contents->GetPrimaryMainFrame()->last_document_url_in_renderer());

    // The window lost its "initial empty document" status but is still on the
    // initial NavigationEntry.
    EXPECT_FALSE(new_root->is_on_initial_empty_document());
    EXPECT_TRUE(last_entry->IsInitialEntry());

    // Navigating the window to |url_2| will be classified as NEW_ENTRY and will
    // replace the previous entry, because it was still on the initial
    // NavigationEntry.
    NavigateWindowAndCheck(
        new_contents, url_2, true /* wait_for_previous_navigations */,
        false /* expect_same_document */, NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY,
        true /* expect_replace */);
    EXPECT_EQ(1, controller.GetEntryCount());
    last_entry = controller.GetLastCommittedEntry();
    // The window is no longer on the initial NavigationEntry.
    EXPECT_FALSE(last_entry->IsInitialEntry());
  }

  // 7) Navigate to |url_2| on a new window that has navigated to a javascript:
  // URL that replaced the initial empty document.
  {
    SCOPED_TRACE(testing::Message() << " Testing case 7.");

    // Create a new window with URL set to a javascript: URL that replaces the
    // document, which will stay on the initial NavigationEntry.
    Shell* new_shell = OpenWindow(contents(), GURL("javascript:'foo'"));
    WebContentsImpl* new_contents =
        static_cast<WebContentsImpl*>(new_shell->web_contents());
    NavigationControllerImpl& controller = new_contents->GetController();
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntryImpl* last_entry = controller.GetLastCommittedEntry();

    // The window should be on the initial empty document and initial
    // NavigationEntry.
    FrameTreeNode* new_root = new_contents->GetPrimaryFrameTree().root();
    EXPECT_TRUE(new_root->is_on_initial_empty_document());
    EXPECT_TRUE(last_entry->IsInitialEntry());

    // Navigating the window to |url_2| will be classified as NEW_ENTRY and will
    // replace the initial entry.
    NavigateWindowAndCheck(new_contents, url_2);
    EXPECT_EQ(1, controller.GetEntryCount());
    last_entry = controller.GetLastCommittedEntry();

    // The window is no longer on the initial empty document and initial
    // NavigationEntry.
    EXPECT_FALSE(new_root->is_on_initial_empty_document());
    EXPECT_FALSE(last_entry->IsInitialEntry());
  }
}

// Ensure the FrameTreeNode's initial empty document status does not become true
// again after it becomes false, even temporarily during a navigation after an
// early RenderFrameHost swap after a renderer crash.
// TODO(https://crbug.com/1072817): This test may become meaningless if we stop
// doing early RFH swaps for crashed frames.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       InitialEmptyDocumentStatusAfterCrash) {
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  EXPECT_TRUE(root->is_on_initial_empty_document());

  // Navigating to a real URL should leave the initial empty document.
  GURL url_1(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url_1));
  EXPECT_FALSE(root->is_on_initial_empty_document());

  // Crash the renderer.
  CrashTab(shell()->web_contents());

  // When starting a new navigation, the early RFH swap should not cause the
  // FrameTreeNode to think it is back on the initial empty document, even
  // temporarily before the new navigation commits.
  GURL url_2(embedded_test_server()->GetURL("/title2.html"));
  TestNavigationManager navigation_manager(shell()->web_contents(), url_2);
  controller.LoadURL(url_2, Referrer(), ui::PAGE_TRANSITION_LINK,
                     std::string());
  EXPECT_TRUE(navigation_manager.WaitForRequestStart());
  EXPECT_FALSE(root->is_on_initial_empty_document());

  ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
  EXPECT_FALSE(root->is_on_initial_empty_document());
}

// Test a same-document navigation in a new window's initial empty document to
// ensure it is classified correctly.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SameDocumentInInitialEmptyDocument) {
  GURL main_window_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_window_url));

  // Create a new blank window that will stay on the initial NavigationEntry.
  Shell* new_shell = OpenBlankWindow(contents());
  WebContentsImpl* new_contents =
      static_cast<WebContentsImpl*>(new_shell->web_contents());
  NavigationControllerImpl& controller = new_contents->GetController();
  FrameTreeNode* root = new_contents->GetPrimaryFrameTree().root();
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_TRUE(controller.GetLastCommittedEntry());

  {
    // Do a same-document navigation.
    LoadCommittedDetailsObserver load_details_observer(new_contents);
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(contents(), "last_opened_window.location.hash='foo';"));
    capturer.Wait();
    load_details_observer.Wait();

    // The pushState will be classified as NEW_ENTRY and will add a new entry.
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_TRUE(controller.GetLastCommittedEntry());

    // Check both NavigationHandle and LoadCommittedDetails for whether this was
    // considered same-document, as these have diverged in the past.
    // See https://crbug.com/1193134.
    EXPECT_TRUE(capturer.is_same_document());
    EXPECT_TRUE(load_details_observer.load_details().is_same_document);
  }
}

// Tests that the initial NavigationEntry retains its "initial" status until the
// first cross-document main frame navigation that is neither a reload nor the
// synchronous about:blank navigation. Tests both the "not for synchronous
// about:blank" and "for synchronous about:blank" cases. Also tests the fact
// that reloads on the initial entry that is not for synchronous about:blank
// will fail.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       InitialNavigationEntryStatus_MainFrame) {
  GURL main_window_url(embedded_test_server()->GetURL("/title1.html"));
  GURL no_commit_url(embedded_test_server()->GetURL("/page204.html"));
  GURL cross_document_url(embedded_test_server()->GetURL("/title2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_window_url));

  // 1) Main frame same-document navigation. Note that we don't test the
  // non-synchronous about:blank case because we can't do same-document
  // navigation if no navigation had committed.
  {
    // Pop open a new window to an empty URL, which will stay at the initial
    // NavigationEntry (but marked as "for synchronous about:blank").
    Shell* new_shell = OpenWindow(contents(), GURL());
    WebContentsImpl* new_contents =
        static_cast<WebContentsImpl*>(new_shell->web_contents());
    NavigationControllerImpl& controller = new_contents->GetController();
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntryImpl* last_entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());
    EXPECT_EQ(GURL("about:blank"), last_entry->GetURL());

    // Navigate the new window to a same-document URL.
    GURL same_document_url("about:blank#foo");
    FrameTreeNode* root = new_contents->GetPrimaryFrameTree().root();
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(NavigateToURL(new_contents, same_document_url));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_TRUE(capturer.is_same_document());

    // We reused the previous NavigationEntry and it keeps its "initial" status.
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_EQ(last_entry, controller.GetLastCommittedEntry());
    last_entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());
    EXPECT_EQ(same_document_url, last_entry->GetURL());
  }

  // 2) Main frame cross-document navigation.
  {
    // 2a: "For synchronous about:blank" case.
    // Pop open a new window to an empty URL, which will stay at the initial
    // NavigationEntry.
    Shell* new_shell = OpenWindow(contents(), GURL());
    WebContentsImpl* new_contents =
        static_cast<WebContentsImpl*>(new_shell->web_contents());
    NavigationControllerImpl& controller = new_contents->GetController();
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntryImpl* last_entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());
    EXPECT_EQ(GURL("about:blank"), last_entry->GetURL());

    // Navigate the new window to a cross-document URL.
    FrameTreeNode* root = new_contents->GetPrimaryFrameTree().root();
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(NavigateToURL(new_contents, cross_document_url));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_FALSE(capturer.is_same_document());

    // We replaced the previous NavigationEntry and it's no longer on the
    // initial NavigationEntry.
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_NE(last_entry, controller.GetLastCommittedEntry());
    last_entry = controller.GetLastCommittedEntry();
    EXPECT_FALSE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kNonInitial,
              last_entry->initial_navigation_entry_state());
    EXPECT_EQ(cross_document_url, last_entry->GetURL());
  }
  {
    // 2b: "Not for synchronous about:blank" case.
    // Pop open a new window which won't commit a navigation, so it will stay at
    // the initial NavigationEntry.
    Shell* new_shell = OpenWindow(contents(), no_commit_url);
    WebContentsImpl* new_contents =
        static_cast<WebContentsImpl*>(new_shell->web_contents());
    // Stop the navigation so that it won't affect future navigations.
    new_contents->Stop();
    NavigationControllerImpl& controller = new_contents->GetController();
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntryImpl* last_entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialNotForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());
    EXPECT_EQ(GURL(), last_entry->GetURL());

    // Navigate the new window to a cross-document URL.
    FrameTreeNode* root = new_contents->GetPrimaryFrameTree().root();
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(NavigateToURL(new_contents, cross_document_url));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_FALSE(capturer.is_same_document());

    // We replaced the previous NavigationEntry and it's no longer on the
    // initial NavigationEntry.
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_NE(last_entry, controller.GetLastCommittedEntry());
    last_entry = controller.GetLastCommittedEntry();
    EXPECT_FALSE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kNonInitial,
              last_entry->initial_navigation_entry_state());
    EXPECT_EQ(cross_document_url, last_entry->GetURL());
  }

  // 3) Main frame reload (browser-initiated).
  {
    // 3a: "For synchronous about:blank" case.
    // Pop open a new window to an empty URL, which will stay at the initial
    // NavigationEntry.
    Shell* new_shell = OpenWindow(contents(), GURL());
    WebContentsImpl* new_contents =
        static_cast<WebContentsImpl*>(new_shell->web_contents());
    NavigationControllerImpl& controller = new_contents->GetController();
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntryImpl* last_entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());
    EXPECT_EQ(GURL("about:blank"), last_entry->GetURL());

    FrameTreeNode* root = new_contents->GetPrimaryFrameTree().root();
    // Reload the new window from the browser. This will succeed because the
    // initial NavigationEntry is for the synchronous about:blank commit.
    FrameNavigateParamsCapturer capturer(root);
    root->current_frame_host()->Reload();
    capturer.Wait();

    // We reused the previous NavigationEntry and it keeps its "initial" status.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_FALSE(capturer.is_same_document());

    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_EQ(last_entry, controller.GetLastCommittedEntry());
    last_entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());
    EXPECT_EQ(GURL("about:blank"), last_entry->GetURL());
  }

  {
    // 3b: "Not for synchronous about:blank" case.
    // Pop open a new window which won't commit a navigation, so it will stay at
    // the initial NavigationEntry.
    Shell* new_shell = OpenWindow(contents(), no_commit_url);
    WebContentsImpl* new_contents =
        static_cast<WebContentsImpl*>(new_shell->web_contents());
    // Stop the navigation so that it won't affect future navigations.
    new_contents->Stop();
    NavigationControllerImpl& controller = new_contents->GetController();
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntryImpl* last_entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialNotForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());
    EXPECT_EQ(GURL(), last_entry->GetURL());

    FrameTreeNode* root = new_contents->GetPrimaryFrameTree().root();
    // Reload the new window from the browser. This will succeed because the
    // initial NavigationEntry is for the synchronous about:blank commit.
    FrameNavigateParamsCapturer capturer(root);
    root->current_frame_host()->Reload();

    // Assert that there's no pending NavigationEntry or NavigationRequest,
    // which means no reload had started.
    EXPECT_FALSE(root->navigation_request());
    EXPECT_FALSE(new_contents->GetController().GetPendingEntry());

    // To ensure that the reload navigation got ignored, trigger another
    // navigation from the renderer. This new navigation would normally get
    // ignored or at least happen later on if a reload is already in progress.
    EXPECT_TRUE(
        ExecJs(root, "location.href = '" + cross_document_url.spec() + "';"));
    capturer.Wait();

    // Assert that the cross-document navigation succeeds and the initial
    // NavigationEntry retains its status.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_FALSE(capturer.is_same_document());

    // We replaced the previous NavigationEntry and it's no longer on the
    // initial NavigationEntry.
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_NE(last_entry, controller.GetLastCommittedEntry());
    last_entry = controller.GetLastCommittedEntry();
    EXPECT_FALSE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kNonInitial,
              last_entry->initial_navigation_entry_state());
    EXPECT_EQ(cross_document_url, last_entry->GetURL());
  }

  // 4) Main frame reload (renderer-initiated). Note that we don't test the
  // "not for synchronous about:blank case here" because even if the reload
  // happens, it won't commit because it will navigate to `no_commit_url`.
  {
    // Pop open a new window to an empty URL, which will stay at the initial
    // NavigationEntry.
    Shell* new_shell = OpenWindow(contents(), GURL());
    WebContentsImpl* new_contents =
        static_cast<WebContentsImpl*>(new_shell->web_contents());
    NavigationControllerImpl& controller = new_contents->GetController();
    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntryImpl* last_entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());
    EXPECT_EQ(GURL("about:blank"), last_entry->GetURL());

    FrameTreeNode* root = new_contents->GetPrimaryFrameTree().root();
    // Reload from the renderer.
    FrameNavigateParamsCapturer capturer(root);
    ASSERT_TRUE(ExecJs(root, "location.reload()"));
    capturer.Wait();

    // We reused the previous NavigationEntry and it keeps its "initial" status.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_FALSE(capturer.is_same_document());

    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_EQ(last_entry, controller.GetLastCommittedEntry());
    last_entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());
    EXPECT_EQ(GURL("about:blank"), last_entry->GetURL());
  }
}

// Tests that the initial NavigationEntry keeps its "initial" status on subframe
// navigations and when restoring/copying history state to another
// NavigationController. Also tests the fact that reloads on the initial entry
// that is not for synchronous about:blank will fail.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       InitialNavigationEntryStatus_SubframeAndRestore) {
  GURL main_window_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  GURL no_commit_url(embedded_test_server()->GetURL("/page204.html"));

  GURL subframe_url(embedded_test_server()->GetURL("/title1.html"));
  GURL subframe_url_2(embedded_test_server()->GetURL("/title2.html"));
  GURL subframe_url_3(embedded_test_server()->GetURL("/title3.html"));

  EXPECT_TRUE(NavigateToURL(shell(), main_window_url));
  // Pop open a new window which won't commit a navigation, so it will stay at
  // the initial NavigationEntry.
  Shell* new_shell = OpenWindow(contents(), no_commit_url);
  WebContentsImpl* new_contents =
      static_cast<WebContentsImpl*>(new_shell->web_contents());
  // Stop the navigation so that it won't affect future navigations.
  new_contents->Stop();
  NavigationControllerImpl& controller = new_contents->GetController();
  NavigationEntryImpl* last_entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_TRUE(last_entry->IsInitialEntry());
  EXPECT_EQ(GURL(), last_entry->GetURL());

  // 1) Subframe creation.
  {
    // Create a subframe and navigate it to `subframe_url`.
    CreateSubframe(new_contents, "child_frame", subframe_url,
                   true /* wait_for_navigation */);

    // The initial NavigationEntry is reused and keeps its "initial" status
    // and added a FrameNavigationEntry for the new subframe.
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_EQ(last_entry, controller.GetLastCommittedEntry());
    EXPECT_TRUE(last_entry->IsInitialEntry());
    ASSERT_EQ(1U, last_entry->root_node()->children.size());
    EXPECT_EQ(subframe_url,
              last_entry->root_node()->children[0]->frame_entry->url());
    EXPECT_EQ(InitialNavigationEntryState::kInitialNotForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());

    EXPECT_EQ(GURL(), last_entry->GetURL());
  }

  FrameTreeNode* root = new_contents->GetPrimaryFrameTree().root();
  FrameTreeNode* child = root->child_at(0);

  // 2) Subframe cross-document navigations.
  {
    // Navigate the subframe to a cross-document URL (browser-initiated).
    {
      FrameNavigateParamsCapturer capturer(child);
      NavigateFrameToURL(child, subframe_url_2);
      capturer.Wait();
      EXPECT_TRUE(capturer.did_replace_entry());
      EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
      EXPECT_EQ(subframe_url_2, child->current_url());
    }

    // The last committed entry retains the "initial NavigationEntry" status
    // and has an updated FrameNavigationEntry for the subframe.
    EXPECT_EQ(1, controller.GetEntryCount());
    last_entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialNotForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());

    EXPECT_EQ(GURL(), last_entry->GetURL());
    EXPECT_EQ(subframe_url_2,
              last_entry->root_node()->children[0]->frame_entry->url());

    {
      FrameNavigateParamsCapturer capturer(child);
      EXPECT_TRUE(
          ExecJs(child, "location.href = '" + subframe_url_3.spec() + "';"));
      capturer.Wait();
      EXPECT_TRUE(capturer.did_replace_entry());
      EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
      EXPECT_EQ(subframe_url_3, child->current_url());
    }

    // The last committed entry retains the "initial NavigationEntry" status.
    EXPECT_EQ(1, controller.GetEntryCount());
    last_entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialNotForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());

    EXPECT_EQ(GURL(), last_entry->GetURL());
    EXPECT_EQ(subframe_url_3,
              last_entry->root_node()->children[0]->frame_entry->url());
  }

  // 3) Subframe reload (renderer-initiated).
  {
    // Reload the subframe from the renderer.
    FrameNavigateParamsCapturer capturer(child);
    ASSERT_TRUE(ExecJs(child, "location.reload()"));
    capturer.Wait();
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
    EXPECT_EQ(subframe_url_3, child->current_url());

    // The last committed entry retains the "initial NavigationEntry" status.
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_EQ(last_entry, controller.GetLastCommittedEntry());
    EXPECT_TRUE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialNotForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());

    EXPECT_EQ(GURL(), last_entry->GetURL());
    EXPECT_EQ(subframe_url_3,
              last_entry->root_node()->children[0]->frame_entry->url());
  }

  // 4) Subframe reload (browser-initiated).
  {
    // Try to reload the subframe from the browser. This will get ignored
    // because the initial NavigationEntry is for the initial empty document
    // (not for the synchronous about:blank commit) and its URL is empty.
    child->current_frame_host()->Reload();

    // Assert that there's no pending NavigationEntry or NavigationRequest,
    // which means no reload had started.
    EXPECT_FALSE(root->navigation_request());
    EXPECT_FALSE(child->navigation_request());
    EXPECT_FALSE(new_contents->GetController().GetPendingEntry());

    // The last committed entry retains the "initial NavigationEntry" status.
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_EQ(last_entry, controller.GetLastCommittedEntry());
    EXPECT_TRUE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialNotForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());

    // To ensure that the reload navigation got ignored, trigger another
    // navigation from the renderer. This new navigation would normally get
    // ignored or at least happen later on if a reload is already in progress.
    FrameNavigateParamsCapturer capturer(child);
    EXPECT_TRUE(
        ExecJs(child, "location.href = '" + subframe_url_2.spec() + "';"));
    capturer.Wait();

    // Assert that the cross-document navigation succeeds and the initial
    // NavigationEntry retains its status.
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
    EXPECT_EQ(subframe_url_2, child->current_url());

    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_EQ(last_entry, controller.GetLastCommittedEntry());
    last_entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(last_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialNotForSynchronousAboutBlank,
              last_entry->initial_navigation_entry_state());

    EXPECT_EQ(GURL(), last_entry->GetURL());
    EXPECT_EQ(subframe_url_2,
              last_entry->root_node()->children[0]->frame_entry->url());
  }

  // 6) Restore/copying history state into a new NavigationController.
  {
    // Restore the initial NavigationEntry in a new tab.
    Shell* restore_shell =
        Shell::CreateNewWindow(controller.GetBrowserContext(),
                               GURL::EmptyGURL(), nullptr, gfx::Size());
    NavigationControllerImpl& restore_controller =
        static_cast<NavigationControllerImpl&>(
            restore_shell->web_contents()->GetController());
    restore_controller.CopyStateFrom(&controller, false /* needs_reload */);

    EXPECT_EQ(1, restore_controller.GetEntryCount());
    EXPECT_EQ(0, restore_controller.GetLastCommittedEntryIndex());
    NavigationEntryImpl* restored_entry =
        restore_controller.GetLastCommittedEntry();

    // The entry keep its "initial" status, and retains its
    // FrameNavigationEntries.
    EXPECT_TRUE(restored_entry->IsInitialEntry());
    EXPECT_EQ(InitialNavigationEntryState::kInitialNotForSynchronousAboutBlank,
              restored_entry->initial_navigation_entry_state());

    EXPECT_EQ(GURL(), restored_entry->root_node()->frame_entry->url());
    ASSERT_EQ(1U, restored_entry->root_node()->children.size());
    EXPECT_EQ(subframe_url_2,
              restored_entry->root_node()->children[0]->frame_entry->url());
  }
}

// Verify that navigations caused by client-side redirects are correctly
// classified.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NavigationTypeClassification_ClientSideRedirect) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(1, controller.GetEntryCount());

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  {
    // Load the redirecting page.
    // Two navigations will happen: The original navigation to the
    // client-redirector URL, then to the URL we got redirected to.
    FrameNavigateParamsCapturer capturer(root);
    capturer.set_navigations_remaining(2);
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/client_redirect.html"));
    NavigateFrameToURL(root, frame_url);
    capturer.Wait();

    ASSERT_EQ(2U, capturer.transitions().size());
    ASSERT_EQ(2U, capturer.navigation_types().size());
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transitions()[0], ui::PAGE_TRANSITION_LINK));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY,
              capturer.navigation_types()[0]);
    EXPECT_FALSE(capturer.did_replace_entries()[0]);
    // The transition used for the second navigation indicates that it is a
    // client-side redirect.
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transitions()[1],
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
                                  ui::PAGE_TRANSITION_CLIENT_REDIRECT)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY,
              capturer.navigation_types()[1]);
    // The client-side redirect results in the replacement of the previous
    // entry.
    EXPECT_TRUE(capturer.did_replace_entries()[1]);
    EXPECT_EQ(2, controller.GetEntryCount());
  }

  {
    GURL fragment_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_1.html#foo"));
    FrameNavigateParamsCapturer capturer(root);
    // Renderer-initiated fragment navigation with location.href
    EXPECT_TRUE(ExecJs(contents(), "location.href = '#foo'"));
    capturer.Wait();
    EXPECT_EQ(fragment_url, contents()->GetLastCommittedURL());
    ASSERT_EQ(1U, capturer.transitions().size());
    // The transition used for the renderer-initiated fragment navigation
    // indicates that it is not a client side redirect since it does
    // not do replacement.
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transitions()[0], ui::PAGE_TRANSITION_LINK));
    EXPECT_FALSE(capturer.did_replace_entries()[0]);
    EXPECT_EQ(3, controller.GetEntryCount());
  }

  {
    GURL fragment_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_1.html#foobar"));
    FrameNavigateParamsCapturer capturer(root);
    // Renderer-initiated fragment navigation with location.replace
    EXPECT_TRUE(ExecJs(contents(), "location.replace('#foobar');"));
    capturer.Wait();
    EXPECT_EQ(fragment_url, contents()->GetLastCommittedURL());
    ASSERT_EQ(1U, capturer.transitions().size());
    // The transition used for the renderer-initiated fragment navigation
    // indicates that it is a client-side redirect that results
    // in entry replacement.
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transitions()[0],
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
                                  ui::PAGE_TRANSITION_CLIENT_REDIRECT)));
    EXPECT_TRUE(capturer.did_replace_entries()[0]);
    EXPECT_EQ(3, controller.GetEntryCount());
  }

  {
    // History API same-document navigation through history.pushState.
    GURL push_state_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_1.html#bar"));
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(
        ExecJs(shell(), "history.pushState({}, '', 'simple_page_1.html#bar')"));
    capturer.Wait();
    EXPECT_EQ(push_state_url, contents()->GetLastCommittedURL());
    ASSERT_EQ(1U, capturer.transitions().size());
    // The transition used for the History API same-document navigation
    // indicates that it is not a client redirect since it doesn't do
    // replacement.
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transitions()[0],
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
                                  ui::PAGE_TRANSITION_CLIENT_REDIRECT)));
    EXPECT_FALSE(capturer.did_replace_entries()[0]);
    EXPECT_EQ(4, controller.GetEntryCount());
  }

  {
    // History API same-document navigation through history.replaceState.
    GURL replace_state_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_1.html#bar2"));
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(
        shell(), "history.replaceState({}, '', 'simple_page_1.html#bar2')"));
    capturer.Wait();
    EXPECT_EQ(replace_state_url, contents()->GetLastCommittedURL());
    ASSERT_EQ(1U, capturer.transitions().size());
    // The transition used for the History API same-document navigation
    // with history.replaceState indicates that it is a client-side redirect
    // that results in entry replacement.
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transitions()[0],
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
                                  ui::PAGE_TRANSITION_CLIENT_REDIRECT)));
    EXPECT_TRUE(capturer.did_replace_entries()[0]);
    EXPECT_EQ(4, controller.GetEntryCount());
  }

  {
    GURL fragment_url_2(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_1.html#baz"));
    // Browser-initiated fragment navigation.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(NavigateToURL(shell(), fragment_url_2));
    capturer.Wait();
    ASSERT_EQ(1U, capturer.transitions().size());
    // The transition used for the browser-initiated fragment navigation does
    // not indicate a client-side redirect.
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transitions()[0],
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_FALSE(capturer.did_replace_entries()[0]);
    EXPECT_EQ(5, controller.GetEntryCount());
  }
}

// Verify that the LoadCommittedDetails::is_same_document value is properly set
// for non same document navigations.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       LoadCommittedDetails_IsSameDocument) {
  GURL links_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_links.html"));
  EXPECT_TRUE(NavigateToURL(shell(), links_url));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  {
    // Do a fragment link click.
    FrameNavigateParamsCapturer capturer(root);
    std::string script = "document.getElementById('fraglink').click()";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_LINK));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());
  }

  {
    // Do a non-fragment link click.
    FrameNavigateParamsCapturer capturer(root);
    std::string script = "document.getElementById('thelink').click()";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_LINK));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());
  }

  // Second verse, same as the first. (But in a subframe.)

  GURL iframe_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), iframe_url));

  root = static_cast<WebContentsImpl*>(shell()->web_contents())
             ->GetPrimaryFrameTree()
             .root();

  ASSERT_EQ(1U, root->child_count());
  ASSERT_NE(nullptr, root->child_at(0));

  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), links_url));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  {
    // Do a fragment link click.
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    std::string script = "document.getElementById('fraglink').click()";
    EXPECT_TRUE(ExecJs(root->child_at(0), script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());
  }

  {
    // Do a non-fragment link click.
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    std::string script = "document.getElementById('thelink').click()";
    EXPECT_TRUE(ExecJs(root->child_at(0), script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());
  }
}

// Verify the tree of FrameNavigationEntries after initial about:blank commits
// in subframes should keep its "initial empty document" status.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_BlankAutoSubframe) {
  GURL about_blank_url(url::kAboutBlankURL);
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  url::Origin main_origin =
      root->current_frame_host()->GetLastCommittedOrigin();
  EXPECT_EQ(url::Origin::Create(main_url), main_origin);

  // 1. Create a iframe with no URL.
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, kAddEmptyFrameScript));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // Check last committed NavigationEntry.
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(main_url, entry->GetURL());
  scoped_refptr<FrameNavigationEntry> root_entry =
      entry->root_node()->frame_entry.get();
  EXPECT_EQ(main_url, root_entry->url());
  EXPECT_EQ(main_origin, root_entry->committed_origin());
  EXPECT_FALSE(root_entry->initiator_origin().has_value());

  // Verify subframe entries.  The entry should now have one blank subframe
  // FrameNavigationEntry and should be on the initial empty document.
  ASSERT_EQ(1U, entry->root_node()->children.size());
  scoped_refptr<FrameNavigationEntry> frame_entry =
      entry->root_node()->children[0]->frame_entry.get();
  EXPECT_EQ(about_blank_url, frame_entry->url());
  EXPECT_EQ(main_origin, frame_entry->committed_origin());
  ASSERT_TRUE(frame_entry->initiator_origin().has_value());
  EXPECT_EQ(main_origin, frame_entry->initiator_origin().value());
  EXPECT_TRUE(root->child_at(0)->is_on_initial_empty_document());

  // 1a. A nested iframe with no URL should also create a subframe entry but not
  // count as a real load.
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root->child_at(0), kAddEmptyFrameScript));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // Verify subframe entries.  The nested entry should have one blank subframe
  // FrameNavigationEntry and should be on the initial empty document.
  ASSERT_EQ(1U, entry->root_node()->children[0]->children.size());
  frame_entry = entry->root_node()->children[0]->children[0]->frame_entry.get();
  EXPECT_EQ(about_blank_url, frame_entry->url());
  EXPECT_EQ(main_origin, frame_entry->committed_origin());
  ASSERT_TRUE(frame_entry->initiator_origin().has_value());
  EXPECT_EQ(main_origin, frame_entry->initiator_origin().value());
  EXPECT_TRUE(root->child_at(0)->child_at(0)->is_on_initial_empty_document());

  // 2. Create another iframe with an explicit about:blank URL.
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, "about:blank")));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // Check last committed NavigationEntry.
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(entry, controller.GetLastCommittedEntry());

  // Verify subframe entries.  The new entry should have one blank subframe
  // FrameNavigationEntry and should be on the initial empty document.
  ASSERT_EQ(2U, entry->root_node()->children.size());
  frame_entry = entry->root_node()->children[1]->frame_entry.get();
  EXPECT_EQ(about_blank_url, frame_entry->url());
  EXPECT_EQ(main_origin, frame_entry->committed_origin());
  ASSERT_TRUE(frame_entry->initiator_origin().has_value());
  EXPECT_EQ(main_origin, frame_entry->initiator_origin().value());
  EXPECT_TRUE(root->child_at(1)->is_on_initial_empty_document());

  // 3. A real same-site navigation in the nested iframe should be AUTO.
  GURL frame_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  {
    LoadCommittedCapturer capturer(root->child_at(0)->child_at(0));
    std::string script = JsReplace(
        "var frames = document.getElementsByTagName('iframe');"
        "frames[0].src = $1;",
        frame_url);
    EXPECT_TRUE(ExecJs(root->child_at(0), script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // Check last committed NavigationEntry.  It should have replaced the previous
  // frame entry in the original NavigationEntry.
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(entry, controller.GetLastCommittedEntry());

  // The entry should still have one nested subframe FrameNavigationEntry.
  ASSERT_EQ(1U, entry->root_node()->children[0]->children.size());
  frame_entry = entry->root_node()->children[0]->children[0]->frame_entry.get();
  EXPECT_EQ(frame_url, frame_entry->url());
  EXPECT_EQ(url::Origin::Create(frame_url), frame_entry->committed_origin());
  ASSERT_TRUE(frame_entry->initiator_origin().has_value());
  EXPECT_EQ(main_origin, frame_entry->initiator_origin().value());
  EXPECT_TRUE(root->child_at(0)->is_on_initial_empty_document());
  EXPECT_FALSE(root->child_at(0)->child_at(0)->is_on_initial_empty_document());
  EXPECT_TRUE(root->child_at(1)->is_on_initial_empty_document());

  // 4. A real cross-site navigation in the second iframe should be AUTO.
  GURL foo_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_2.html"));
  {
    LoadCommittedCapturer capturer(root->child_at(1));
    std::string script = JsReplace(
        "var frames = document.getElementsByTagName('iframe');"
        "frames[1].src = $1;",
        foo_url);
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // Check last committed NavigationEntry.
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(entry, controller.GetLastCommittedEntry());

  // The entry should still have two subframe FrameNavigationEntries.
  ASSERT_EQ(2U, entry->root_node()->children.size());
  frame_entry = entry->root_node()->children[1]->frame_entry.get();
  EXPECT_EQ(foo_url, frame_entry->url());
  EXPECT_EQ(url::Origin::Create(foo_url), frame_entry->committed_origin());
  ASSERT_TRUE(frame_entry->initiator_origin().has_value());
  EXPECT_EQ(main_origin, frame_entry->initiator_origin().value());
  EXPECT_TRUE(root->child_at(0)->is_on_initial_empty_document());
  EXPECT_FALSE(root->child_at(0)->child_at(0)->is_on_initial_empty_document());
  EXPECT_FALSE(root->child_at(1)->is_on_initial_empty_document());
  EXPECT_EQ(frame_entry->committed_origin(),
            root->child_at(1)->current_frame_host()->GetLastCommittedOrigin());

  // 5. A new navigation to about:blank in the nested frame should count as a
  // real load, since that frame has already committed a real load and this is
  // not the initial blank page.
  {
    LoadCommittedCapturer capturer(root->child_at(0)->child_at(0));
    std::string script =
        "var frames = document.getElementsByTagName('iframe');"
        "frames[0].src = 'about:blank';";
    EXPECT_TRUE(ExecJs(root->child_at(0), script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
  }

  // This should have created a new NavigationEntry.
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_NE(entry, controller.GetLastCommittedEntry());
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();

  // Verify subframe entries are shared between the NavigationEntries.
  ASSERT_EQ(2U, entry->root_node()->children.size());
  EXPECT_EQ(entry->root_node()->frame_entry.get(),
            entry2->root_node()->frame_entry.get());
  EXPECT_EQ(entry->root_node()->children[0]->frame_entry.get(),
            entry2->root_node()->children[0]->frame_entry.get());
  EXPECT_EQ(entry->root_node()->children[1]->frame_entry.get(),
            entry2->root_node()->children[1]->frame_entry.get());
  EXPECT_NE(entry->root_node()->children[0]->children[0]->frame_entry.get(),
            entry2->root_node()->children[0]->children[0]->frame_entry.get());
  frame_entry =
      entry2->root_node()->children[0]->children[0]->frame_entry.get();
  EXPECT_EQ(about_blank_url, frame_entry->url());
  EXPECT_EQ(main_origin, frame_entry->committed_origin());
  ASSERT_TRUE(frame_entry->initiator_origin().has_value());
  EXPECT_EQ(main_origin, frame_entry->initiator_origin().value());
  EXPECT_TRUE(root->child_at(0)->is_on_initial_empty_document());
  EXPECT_FALSE(root->child_at(0)->child_at(0)->is_on_initial_empty_document());
  EXPECT_FALSE(root->child_at(1)->is_on_initial_empty_document());

  // Check the end result of the frame tree.
  if (AreAllSitesIsolatedForTesting()) {
    EXPECT_EQ(
        " Site A ------------ proxies for B\n"
        "   |--Site A ------- proxies for B\n"
        "   |    +--Site A -- proxies for B\n"
        "   +--Site B ------- proxies for A\n"
        "Where A = http://127.0.0.1/\n"
        "      B = http://foo.com/",
        DepictFrameTree(*root));
  }
}

// Verify the tree of FrameNavigationEntries when a nested iframe commits inside
// the initial blank page of a loading iframe.  Prevents regression of
// https://crbug.com/600743.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_SlowNestedAutoSubframe) {
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1. Create a iframe with a URL that doesn't commit.
  GURL slow_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  TestNavigationManager subframe_delayer(shell()->web_contents(), slow_url);
  std::string script_template =
      "var iframe = document.createElement('iframe');"
      "iframe.src = $1;"
      "document.body.appendChild(iframe);";

  EXPECT_TRUE(ExecJs(root, JsReplace(script_template, slow_url)));
  EXPECT_TRUE(subframe_delayer.WaitForRequestStart());

  // Stop the request so that we can wait for load stop below, without ending up
  // with a commit for this frame.
  shell()->web_contents()->Stop();

  // 2. A nested iframe with a cross-site URL should be able to commit.
  GURL foo_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(ExecJs(root->child_at(0), JsReplace(script_template, foo_url)));
  WaitForLoadStopWithoutSuccessCheck(shell()->web_contents());

  // TODO(creis): Check subframe entries once we create them in this case.
  // See https://crbug.com/608402.
  EXPECT_EQ(foo_url, root->child_at(0)->child_at(0)->current_url());
}

// Verify that history.pushState() does not replace the pending entry.
// https://crbug.com/900036.
// TODO(crbug.com/926009): Fix and re-enable this test.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       DISABLED_PushStatePreservesPendingEntry) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1. Start loading an URL that doesn't commit.
  GURL stalled_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  // Have the user decide to go to a different page which is very slow.
  TestNavigationManager stalled_navigation(shell()->web_contents(),
                                           stalled_url);
  controller.LoadURL(stalled_url, Referrer(), ui::PAGE_TRANSITION_LINK,
                     std::string());
  EXPECT_TRUE(stalled_navigation.WaitForRequestStart());

  // That should be the pending entry.
  NavigationEntryImpl* entry = controller.GetPendingEntry();
  ASSERT_NE(nullptr, entry);
  EXPECT_EQ(stalled_url, entry->GetURL());

  {
    // Now the existing page uses history.pushState() while the pending entry
    // for the other navigation still exists.
    FrameNavigateParamsCapturer capturer(root);
    capturer.set_wait_for_load(false);
    std::string script = "history.pushState({}, '', 'pushed')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());
  }

  // This should not replace the pending entry.
  entry = controller.GetPendingEntry();
  ASSERT_NE(nullptr, entry);
  EXPECT_EQ(stalled_url, entry->GetURL());
}

// Verify that history.replaceState() does not replace the pending entry.
// https://crbug.com/900036.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ReplaceStatePreservesPendingEntry) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1. Start loading an URL that doesn't commit.
  GURL stalled_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  // Have the user decide to go to a different page which is very slow.
  TestNavigationManager stalled_navigation(shell()->web_contents(),
                                           stalled_url);
  controller.LoadURL(stalled_url, Referrer(), ui::PAGE_TRANSITION_LINK,
                     std::string());
  EXPECT_TRUE(stalled_navigation.WaitForRequestStart());

  // That should be the pending entry.
  NavigationEntryImpl* entry = controller.GetPendingEntry();
  ASSERT_NE(nullptr, entry);
  EXPECT_EQ(stalled_url, entry->GetURL());

  {
    // Now the existing page uses history.replaceState() while the pending entry
    // for the other navigation still exists.
    FrameNavigateParamsCapturer capturer(root);
    capturer.set_wait_for_load(false);
    std::string script = "history.replaceState({}, '', 'replaced')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());
  }

  // This should not replace the pending entry.
  entry = controller.GetPendingEntry();
  ASSERT_NE(nullptr, entry);
  EXPECT_EQ(stalled_url, entry->GetURL());
}

// Verify the tree of FrameNavigationEntries when a nested iframe commits inside
// the initial blank page of an iframe with no committed entry.  Prevents
// regression of https://crbug.com/600743.
// Flaky test: See https://crbug.com/610801
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    DISABLED_FrameNavigationEntry_NoCommitNestedAutoSubframe) {
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1. Create a iframe with a URL that doesn't commit.
  GURL no_commit_url(embedded_test_server()->GetURL("/nocontent"));
  {
    std::string script =
        "var iframe = document.createElement('iframe');"
        "iframe.src = '" +
        no_commit_url.spec() +
        "';"
        "document.body.appendChild(iframe);";
    EXPECT_TRUE(ExecJs(root, script));
  }
  EXPECT_EQ(GURL(), root->child_at(0)->current_url());

  // 2. A nested iframe with a cross-site URL should be able to commit.
  GURL foo_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_1.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(
        ExecJs(root->child_at(0), JsReplace(kAddFrameWithSrcScript, foo_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // TODO(creis): Check subframe entries once we create them in this case.
  // See https://crbug.com/608402.
  EXPECT_EQ(foo_url, root->child_at(0)->child_at(0)->current_url());
}

// Verify the tree of FrameNavigationEntries when a nested iframe commits after
// doing same document back navigation, in which case its parent might not have
// been in the NavigationEntry.  Prevents regression of
// https://crbug.com/600743.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_BackNestedAutoSubframe) {
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1. Perform same document navigation.
  {
    FrameNavigateParamsCapturer capturer(root);
    std::string script = "history.pushState({}, 'foo', 'foo')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());
  }

  // 2. Create an iframe.
  GURL child_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, child_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // 3. Perform same document back navigation.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }

  // 4. A nested iframe with a cross-site URL should be able to commit.
  GURL grandchild_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_1.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root->child_at(0),
                       JsReplace(kAddFrameWithSrcScript, grandchild_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // TODO(creis): Check subframe entries once we create them in this case.
  // See https://crbug.com/608402.
  EXPECT_EQ(grandchild_url, root->child_at(0)->child_at(0)->current_url());
}

// Verify that an iframe is not reloaded after going back same-document to a
// NavigationEntry before the iframe was created, and then forward again.
// Prevents regression of https://crbug.com/1301985.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_BackForwardAcrossSubframeCreation) {
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1. Perform a same-document navigation.
  {
    FrameNavigateParamsCapturer capturer(root);
    std::string script = "history.pushState({}, 'foo', 'foo')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());
  }

  // 2. Create an iframe to about:blank and inject content.
  GURL blank_url(url::kAboutBlankURL);
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, blank_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
    EXPECT_TRUE(ExecJs(root->child_at(0), "foo=3;"));
  }

  // 3. Perform a same-document back navigation, to a NavigationEntry before the
  // iframe existed.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }

  // 4. Perform a same-document forward navigation. Wait for all navigations to
  // stop, in case subframes are navigated as well.
  {
    TestNavigationObserver forward_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoForward();
    forward_load_observer.Wait();
    EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  }

  // Ensure the injected content is still present in the iframe to verify that
  // it was not reloaded. See https://crbug.com/1301985.
  EXPECT_EQ(3, EvalJs(root->child_at(0), "foo"));
}

// Verify that an iframe ends up at the correct URL if going forward from a
// NavigationEntry created before it existed to one where its URL must change.
// See https://crbug.com/1301985 and
// FrameNavigationEntry_BackForwardAcrossSubframeCreation.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_BackForwardAcrossSubframeCreationCrossDocument) {
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1. Perform a same-document navigation.
  {
    FrameNavigateParamsCapturer capturer(root);
    std::string script = "history.pushState({}, 'foo', 'foo')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());
  }

  // 2. Create an iframe to about:blank and inject content.
  GURL child_url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, child_url1)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
    EXPECT_TRUE(ExecJs(root->child_at(0), "foo=3;"));
  }

  // 3. Navigate the iframe to another document.
  GURL child_url2(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/page_with_links.html"));
  {
    LoadCommittedCapturer capturer(root->child_at(0));
    std::string script = JsReplace("frames[0].location = $1;", child_url2);
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
  }
  EXPECT_EQ(child_url2, root->child_at(0)->current_url());

  // 4. Perform a cross-document back navigation within the iframe.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }
  EXPECT_EQ(child_url1, root->child_at(0)->current_url());

  // 5. Perform a same-document back navigation, to a NavigationEntry before the
  // iframe existed.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }
  EXPECT_EQ(child_url1, root->child_at(0)->current_url());

  // 6. Go forward two entries (cross-document in the iframe) and wait for all
  // navigations to stop. Checking the URL in this case ensures that we do not
  // incorrectly skip navigating the iframe if it needs to be navigated.
  {
    TestNavigationObserver forward_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoToOffset(2);
    forward_load_observer.Wait();
    EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  }
  EXPECT_EQ(child_url2, root->child_at(0)->current_url());
}

// Verify that the main frame can navigate a grandchild frame to about:blank,
// even if GetFrameEntry might not find the corresponding FrameNavigationEntry
// due to https://crbug.com/608402.  Prevents regression of
// https://crbug.com/1054209.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_BackSameDocumentThenNestedBlank) {
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1. Perform same document navigation.
  {
    FrameNavigateParamsCapturer capturer(root);
    std::string script = "history.pushState({}, 'foo', 'foo')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());
  }

  // 2. Create an iframe.
  GURL child_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, child_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // 3. Create a cross-process nested iframe.
  GURL grandchild_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_1.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root->child_at(0),
                       JsReplace(kAddFrameWithSrcScript, grandchild_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // 4. Perform same document back navigation.  The subframes are still present
  // but the previous NavigationEntry doesn't have a record of them (per
  // https://crbug.com/608402).
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }

  // 5. Navigate the nested iframe to about:blank in the main frame's process.
  // This should end up in the main frame's process (without crashing).
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0)->child_at(0));
    std::string script = "frames[0][0].location.href = 'about:blank'";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());
  }

  // TODO(creis): Check subframe entries once we create them in this case.
  // See https://crbug.com/608402.
  EXPECT_EQ(GURL(url::kAboutBlankURL),
            root->child_at(0)->child_at(0)->current_url());
  EXPECT_EQ(
      root->current_frame_host()->GetSiteInstance(),
      root->child_at(0)->child_at(0)->current_frame_host()->GetSiteInstance());
}

// Verify the tree of FrameNavigationEntries when a nested iframe commits after
// its parent changes its name, in which case we might not find the parent
// FrameNavigationEntry.  Prevents regression of https://crbug.com/600743.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_RenameNestedAutoSubframe) {
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1. Create an iframe.
  GURL child_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, child_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // 2. Change the iframe's name.
  EXPECT_TRUE(ExecJs(root->child_at(0), "window.name = 'foo';"));

  // 3. A nested iframe with a cross-site URL should be able to commit.
  GURL bar_url(embedded_test_server()->GetURL(
      "bar.com", "/navigation_controller/simple_page_1.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(
        ExecJs(root->child_at(0), JsReplace(kAddFrameWithSrcScript, bar_url)));

    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // TODO(creis): Check subframe entries once we create them in this case.
  // See https://crbug.com/608402.
  EXPECT_EQ(bar_url, root->child_at(0)->child_at(0)->current_url());
}

IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_CrossProcessSameDocumentNavigation) {
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  // 1. Create a cross-process iframe.
  GURL child_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_2.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, child_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // 2. Create a grandchild iframe (also cross-process).
  GURL grandchild_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_1.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root->child_at(0),
                       JsReplace(kAddFrameWithSrcScript, grandchild_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  NavigationEntryImpl* entry_before_nav = controller.GetLastCommittedEntry();
  scoped_refptr<FrameNavigationEntry> main_entry_before_nav =
      entry_before_nav->GetFrameEntry(root);
  scoped_refptr<FrameNavigationEntry> child_entry_before_nav =
      entry_before_nav->GetFrameEntry(root->child_at(0));
  scoped_refptr<FrameNavigationEntry> grandchild_entry_before_nav =
      entry_before_nav->GetFrameEntry(root->child_at(0)->child_at(0));
  ASSERT_NE(main_entry_before_nav, nullptr);
  ASSERT_NE(child_entry_before_nav, nullptr);
  ASSERT_NE(grandchild_entry_before_nav, nullptr);

  // 4. The main frame navigates the child same-document from a different
  // process.
  GURL child_fragment_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_2.html#fragment"));
  {
    LoadCommittedCapturer capturer(root->child_at(0));
    std::string script =
        JsReplace("frames[0].location = $1;", child_fragment_url);
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
  }

  NavigationEntryImpl* entry_after_nav = controller.GetLastCommittedEntry();
  // Only the navigated frame's FrameNavigationEntry should change.
  EXPECT_EQ(main_entry_before_nav, entry_after_nav->GetFrameEntry(root));
  EXPECT_NE(child_entry_before_nav,
            entry_after_nav->GetFrameEntry(root->child_at(0)));
  EXPECT_EQ(child_fragment_url,
            entry_after_nav->GetFrameEntry(root->child_at(0))->url());
  EXPECT_EQ(grandchild_entry_before_nav,
            entry_after_nav->GetFrameEntry(root->child_at(0)->child_at(0)));
}

// Verify the tree of FrameNavigationEntries after NAVIGATION_TYPE_AUTO_SUBFRAME
// commits.
// TODO(creis): Test updating entries for history auto subframe navigations.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_AutoSubframe) {
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1. Create a same-site iframe.
  GURL frame_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, frame_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // Check last committed NavigationEntry.
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(main_url, entry->GetURL());
  scoped_refptr<FrameNavigationEntry> root_entry =
      entry->root_node()->frame_entry.get();
  EXPECT_EQ(main_url, root_entry->url());
  EXPECT_FALSE(root_entry->initiator_origin().has_value());
  EXPECT_FALSE(controller.GetPendingEntry());

  // The entry should now have a subframe FrameNavigationEntry.
  ASSERT_EQ(1U, entry->root_node()->children.size());
  scoped_refptr<FrameNavigationEntry> frame_entry =
      entry->root_node()->children[0]->frame_entry.get();
  EXPECT_EQ(frame_url, frame_entry->url());
  ASSERT_TRUE(frame_entry->initiator_origin().has_value());
  EXPECT_EQ(url::Origin::Create(main_url),
            frame_entry->initiator_origin().value());
  EXPECT_FALSE(root->child_at(0)->is_on_initial_empty_document());

  // 2. Create a second, initially cross-site iframe.
  GURL foo_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_1.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, foo_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // The last committed NavigationEntry shouldn't have changed.
  EXPECT_EQ(1, controller.GetEntryCount());
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(main_url, entry->GetURL());
  root_entry = entry->root_node()->frame_entry.get();
  EXPECT_EQ(main_url, root_entry->url());
  EXPECT_FALSE(root_entry->initiator_origin().has_value());
  EXPECT_FALSE(controller.GetPendingEntry());

  // The entry should now have 2 subframe FrameNavigationEntries.
  ASSERT_EQ(2U, entry->root_node()->children.size());
  frame_entry = entry->root_node()->children[1]->frame_entry.get();
  EXPECT_EQ(foo_url, frame_entry->url());
  ASSERT_TRUE(frame_entry->initiator_origin().has_value());
  EXPECT_EQ(url::Origin::Create(main_url),
            frame_entry->initiator_origin().value());
  EXPECT_FALSE(root->child_at(1)->is_on_initial_empty_document());

  // 3. Create a nested iframe in the second subframe.
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(
        ExecJs(root->child_at(1), JsReplace(kAddFrameWithSrcScript, foo_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // The last committed NavigationEntry shouldn't have changed.
  EXPECT_EQ(1, controller.GetEntryCount());
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(main_url, entry->GetURL());
  root_entry = entry->root_node()->frame_entry.get();
  EXPECT_EQ(main_url, root_entry->url());
  EXPECT_FALSE(root_entry->initiator_origin().has_value());

  // The entry should now have 2 subframe FrameNavigationEntries.
  ASSERT_EQ(2U, entry->root_node()->children.size());
  ASSERT_EQ(1U, entry->root_node()->children[1]->children.size());
  frame_entry = entry->root_node()->children[1]->children[0]->frame_entry.get();
  EXPECT_EQ(foo_url, frame_entry->url());
  ASSERT_TRUE(frame_entry->initiator_origin().has_value());
  EXPECT_EQ(url::Origin::Create(foo_url),
            frame_entry->initiator_origin().value());

  // 4. Create a third iframe on the same site as the second.  This ensures that
  // the commit type is correct even when the subframe process already exists.
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, foo_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // The last committed NavigationEntry shouldn't have changed.
  EXPECT_EQ(1, controller.GetEntryCount());
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(main_url, entry->GetURL());
  root_entry = entry->root_node()->frame_entry.get();
  EXPECT_EQ(main_url, root_entry->url());
  EXPECT_FALSE(root_entry->initiator_origin().has_value());

  // The entry should now have 3 subframe FrameNavigationEntries.
  ASSERT_EQ(3U, entry->root_node()->children.size());
  frame_entry = entry->root_node()->children[2]->frame_entry.get();
  EXPECT_EQ(foo_url, frame_entry->url());
  ASSERT_TRUE(frame_entry->initiator_origin().has_value());
  EXPECT_EQ(url::Origin::Create(main_url),
            frame_entry->initiator_origin().value());

  // 5. Create a nested iframe on the original site (A-B-A).
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    FrameTreeNode* child = root->child_at(2);
    EXPECT_TRUE(ExecJs(child, JsReplace(kAddFrameWithSrcScript, frame_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // The last committed NavigationEntry shouldn't have changed.
  EXPECT_EQ(1, controller.GetEntryCount());
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(main_url, entry->GetURL());
  root_entry = entry->root_node()->frame_entry.get();
  EXPECT_EQ(main_url, root_entry->url());
  EXPECT_FALSE(root_entry->initiator_origin().has_value());

  // There should be a corresponding FrameNavigationEntry.
  ASSERT_EQ(1U, entry->root_node()->children[2]->children.size());
  frame_entry = entry->root_node()->children[2]->children[0]->frame_entry.get();
  EXPECT_EQ(frame_url, frame_entry->url());
  ASSERT_TRUE(frame_entry->initiator_origin().has_value());
  EXPECT_EQ(url::Origin::Create(foo_url),
            frame_entry->initiator_origin().value());

  // Check the end result of the frame tree.
  if (AreAllSitesIsolatedForTesting()) {
    EXPECT_EQ(
        " Site A ------------ proxies for B\n"
        "   |--Site A ------- proxies for B\n"
        "   |--Site B ------- proxies for A\n"
        "   |    +--Site B -- proxies for A\n"
        "   +--Site B ------- proxies for A\n"
        "        +--Site A -- proxies for B\n"
        "Where A = http://127.0.0.1/\n"
        "      B = http://foo.com/",
        DepictFrameTree(*root));
  }
}

// Verify the tree of FrameNavigationEntries after NAVIGATION_TYPE_NEW_SUBFRAME
// commits.
// Disabled due to flakes; see https://crbug.com/646836.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_NewSubframe) {
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1. Create a same-site iframe.
  GURL frame_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, frame_url)));
    capturer.Wait();
  }
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();

  // 2. Navigate in the subframe same-site.
  GURL frame_url2(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_links.html"));
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url2));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
  }

  // We should have created a new NavigationEntry with the same main frame URL.
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();
  EXPECT_NE(entry, entry2);
  EXPECT_EQ(main_url, entry2->GetURL());
  scoped_refptr<FrameNavigationEntry> root_entry2 =
      entry2->root_node()->frame_entry.get();
  EXPECT_EQ(entry->root_node()->frame_entry.get(), root_entry2);
  EXPECT_EQ(main_url, root_entry2->url());
  EXPECT_FALSE(root_entry2->initiator_origin().has_value());

  // The entry should have a new FrameNavigationEntries for the subframe.
  ASSERT_EQ(1U, entry2->root_node()->children.size());
  EXPECT_NE(entry->root_node()->children[0]->frame_entry.get(),
            entry2->root_node()->children[0]->frame_entry.get());
  EXPECT_EQ(frame_url2, entry2->root_node()->children[0]->frame_entry->url());

  // 3. Create a second, initially cross-site iframe.
  GURL foo_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_1.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, foo_url)));
    capturer.Wait();
  }

  // 4. Create a nested same-site iframe in the second subframe, wait for it to
  // commit, then navigate it again cross-site.
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(
        ExecJs(root->child_at(1), JsReplace(kAddFrameWithSrcScript, foo_url)));
    capturer.Wait();
  }
  GURL bar_url(embedded_test_server()->GetURL(
      "bar.com", "/navigation_controller/simple_page_1.html"));
  {
    FrameNavigateParamsCapturer capturer(root->child_at(1)->child_at(0));
    RenderFrameDeletedObserver deleted_observer(
        root->child_at(1)->child_at(0)->current_frame_host());
    EXPECT_TRUE(
        NavigateToURLFromRenderer(root->child_at(1)->child_at(0), bar_url));
    // Wait for the RenderFrame to go away, if this will be cross-process.
    if (AreAllSitesIsolatedForTesting())
      deleted_observer.WaitUntilDeleted();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
  }

  // We should have created a new NavigationEntry with the same main frame URL.
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry3 = controller.GetLastCommittedEntry();
  EXPECT_NE(entry, entry3);
  EXPECT_EQ(main_url, entry3->GetURL());
  scoped_refptr<FrameNavigationEntry> root_entry3 =
      entry3->root_node()->frame_entry.get();
  EXPECT_EQ(main_url, root_entry3->url());
  EXPECT_EQ(root_entry2, root_entry3);

  // The entry should still have FrameNavigationEntries for all 3 subframes.
  ASSERT_EQ(2U, entry3->root_node()->children.size());
  EXPECT_EQ(entry2->root_node()->children[0]->frame_entry.get(),
            entry3->root_node()->children[0]->frame_entry.get());
  EXPECT_EQ(entry2->root_node()->children[1]->frame_entry.get(),
            entry3->root_node()->children[1]->frame_entry.get());
  EXPECT_NE(entry2->root_node()->children[1]->children[0]->frame_entry.get(),
            entry3->root_node()->children[1]->children[0]->frame_entry.get());
  EXPECT_EQ(foo_url, entry3->root_node()->children[1]->frame_entry->url());
  ASSERT_EQ(1U, entry3->root_node()->children[1]->children.size());
  EXPECT_EQ(bar_url,
            entry3->root_node()->children[1]->children[0]->frame_entry->url());

  // 6. Navigate the second subframe cross-site, clearing its existing subtree.
  GURL baz_url(embedded_test_server()->GetURL(
      "baz.com", "/navigation_controller/simple_page_1.html"));
  {
    FrameNavigateParamsCapturer capturer(root->child_at(1));
    RenderFrameDeletedObserver deleted_observer(
        root->child_at(1)->current_frame_host());
    std::string script =
        "var frames = document.getElementsByTagName('iframe');"
        "frames[1].src = '" +
        baz_url.spec() + "';";
    EXPECT_TRUE(ExecJs(root, script));
    // Wait for the RenderFrame to go away, if this will be cross-process.
    if (AreAllSitesIsolatedForTesting())
      deleted_observer.WaitUntilDeleted();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
  }

  // We should have created a new NavigationEntry with the same main frame URL.
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(3, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry4 = controller.GetLastCommittedEntry();
  EXPECT_NE(entry, entry4);
  EXPECT_EQ(main_url, entry4->GetURL());
  scoped_refptr<FrameNavigationEntry> root_entry4 =
      entry4->root_node()->frame_entry.get();
  EXPECT_EQ(root_entry3, root_entry4);
  EXPECT_EQ(main_url, root_entry4->url());

  // The entry should still have FrameNavigationEntries for all 3 subframes.
  ASSERT_EQ(2U, entry4->root_node()->children.size());
  EXPECT_EQ(entry3->root_node()->children[0]->frame_entry.get(),
            entry4->root_node()->children[0]->frame_entry.get());
  EXPECT_EQ(baz_url, entry4->root_node()->children[1]->frame_entry->url());
  ASSERT_EQ(0U, entry4->root_node()->children[1]->children.size());

  // Check the end result of the frame tree.
  if (AreAllSitesIsolatedForTesting()) {
    EXPECT_EQ(
        " Site A ------------ proxies for B\n"
        "   |--Site A ------- proxies for B\n"
        "   +--Site B ------- proxies for A\n"
        "Where A = http://127.0.0.1/\n"
        "      B = http://baz.com/",
        DepictFrameTree(*root));
  }
}

// Ensure that we don't crash when navigating subframes after same document
// navigations.  See https://crbug.com/522193.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_SubframeAfterSameDocument) {
  // 1. Start on a page with a subframe.
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  ASSERT_EQ(1U, root->child_count());
  ASSERT_NE(nullptr, root->child_at(0));

  // Navigate to a real page in the subframe, so that the next navigation will
  // be MANUAL_SUBFRAME.
  GURL subframe_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  {
    LoadCommittedCapturer capturer(root->child_at(0));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), subframe_url));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // 2. Same document navigation in the main frame.
  std::string push_script = "history.pushState({}, 'page 2', 'page_2.html')";
  EXPECT_TRUE(ExecJs(root, push_script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  // The entry should have a FrameNavigationEntry for the subframe.
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  ASSERT_EQ(1U, entry->root_node()->children.size());
  EXPECT_EQ(subframe_url, entry->root_node()->children[0]->frame_entry->url());

  // 3. Add a nested subframe.
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root->child_at(0),
                       JsReplace(kAddFrameWithSrcScript, subframe_url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // The entry should have a FrameNavigationEntry for the subframe.
  entry = controller.GetLastCommittedEntry();
  ASSERT_EQ(1U, entry->root_node()->children.size());
  EXPECT_EQ(subframe_url, entry->root_node()->children[0]->frame_entry->url());
  ASSERT_EQ(1U, entry->root_node()->children[0]->children.size());
  EXPECT_EQ(subframe_url,
            entry->root_node()->children[0]->children[0]->frame_entry->url());
}

// Verify the tree of FrameNavigationEntries after back/forward navigations in a
// cross-site subframe.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_SubframeBackForward) {
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1. Create a same-site iframe.
  GURL frame_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, frame_url)));
    capturer.Wait();
  }
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();

  // 2. Navigate in the subframe cross-site.
  GURL frame_url2(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/page_with_links.html"));
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url2));
    capturer.Wait();
  }
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();

  // 3. Navigate in the subframe cross-site again.
  GURL frame_url3(embedded_test_server()->GetURL(
      "bar.com", "/navigation_controller/page_with_links.html"));
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url3));
    capturer.Wait();
  }
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry3 = controller.GetLastCommittedEntry();
  EXPECT_EQ(entry1->root_node()->frame_entry.get(),
            entry2->root_node()->frame_entry.get());
  EXPECT_EQ(entry2->root_node()->frame_entry.get(),
            entry3->root_node()->frame_entry.get());
  EXPECT_NE(entry1->root_node()->children[0]->frame_entry.get(),
            entry2->root_node()->children[0]->frame_entry.get());
  EXPECT_NE(entry1->root_node()->children[0]->frame_entry.get(),
            entry3->root_node()->children[0]->frame_entry.get());
  EXPECT_NE(entry2->root_node()->children[0]->frame_entry.get(),
            entry3->root_node()->children[0]->frame_entry.get());

  // 4. Go back in the subframe.
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    shell()->web_contents()->GetController().GoBack();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
  }
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry2, controller.GetLastCommittedEntry());

  // The entry should have a FrameNavigationEntry for the subframe.
  ASSERT_EQ(1U, entry2->root_node()->children.size());
  EXPECT_EQ(frame_url2, entry2->root_node()->children[0]->frame_entry->url());

  // 5. Go back in the subframe again to the parent page's site.
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    shell()->web_contents()->GetController().GoBack();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
  }
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry1, controller.GetLastCommittedEntry());

  // The entry should have a FrameNavigationEntry for the subframe.
  ASSERT_EQ(1U, entry1->root_node()->children.size());
  EXPECT_EQ(frame_url, entry1->root_node()->children[0]->frame_entry->url());

  // 6. Go forward in the subframe cross-site.
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    shell()->web_contents()->GetController().GoForward();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
  }
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry2, controller.GetLastCommittedEntry());

  // The entry should have a FrameNavigationEntry for the subframe.
  ASSERT_EQ(1U, entry2->root_node()->children.size());
  EXPECT_EQ(frame_url2, entry2->root_node()->children[0]->frame_entry->url());

  // 7. Go forward in the subframe again, cross-site.
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    shell()->web_contents()->GetController().GoForward();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
  }
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry3, controller.GetLastCommittedEntry());

  // The entry should have a FrameNavigationEntry for the subframe.
  ASSERT_EQ(1U, entry3->root_node()->children.size());
  EXPECT_EQ(frame_url3, entry3->root_node()->children[0]->frame_entry->url());
}

// Verify the tree of FrameNavigationEntries after subframes are recreated in
// history navigations, including nested frames.  The history will look like:
// 1. initial_url
// 2. main_url_a (data_url)
// 3. main_url_a (frame_url_b (data_url))
// 4. main_url_a (frame_url_b (frame_url_c))
// 5. main_url_d
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_RecreatedSubframeBackForward) {
  // 1. Start on a page with no frames.
  GURL initial_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), initial_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_EQ(initial_url, root->current_url());
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();
  EXPECT_EQ(0U, entry1->root_node()->children.size());

  // 2. Navigate to a page with a data URL iframe.
  GURL main_url_a(embedded_test_server()->GetURL(
      "a.com", "/navigation_controller/page_with_data_iframe.html"));
  GURL data_url("data:text/html,Subframe");
  EXPECT_TRUE(NavigateToURL(shell(), main_url_a));
  ASSERT_EQ(1U, root->child_count());
  ASSERT_EQ(0U, root->child_at(0)->child_count());
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->current_url());

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();

  // The entry should have a FrameNavigationEntry for the data subframe.
  ASSERT_EQ(1U, entry2->root_node()->children.size());
  EXPECT_EQ(data_url, entry2->root_node()->children[0]->frame_entry->url());
  EXPECT_EQ(entry2->root_node()
                ->frame_entry->committed_origin()
                ->GetTupleOrPrecursorTupleIfOpaque(),
            entry2->root_node()
                ->children[0]
                ->frame_entry->committed_origin()
                ->GetTupleOrPrecursorTupleIfOpaque());
  ASSERT_TRUE(entry2->root_node()
                  ->children[0]
                  ->frame_entry->initiator_origin()
                  .has_value());
  EXPECT_EQ(url::Origin::Create(main_url_a),
            entry2->root_node()
                ->children[0]
                ->frame_entry->initiator_origin()
                .value());

  // 3. Navigate the iframe cross-site to a page with a nested iframe.
  GURL frame_url_b(embedded_test_server()->GetURL(
      "b.com", "/navigation_controller/page_with_data_iframe.html"));
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url_b));
    capturer.Wait();
  }
  ASSERT_EQ(1U, root->child_count());
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ(frame_url_b, root->child_at(0)->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->child_at(0)->current_url());

  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry3 = controller.GetLastCommittedEntry();

  // The entry should have a FrameNavigationEntry for the b.com subframe.
  ASSERT_EQ(1U, entry3->root_node()->children.size());
  ASSERT_EQ(1U, entry3->root_node()->children[0]->children.size());
  EXPECT_EQ(entry2->root_node()->frame_entry.get(),
            entry3->root_node()->frame_entry.get());
  EXPECT_NE(entry2->root_node()->children[0]->frame_entry.get(),
            entry3->root_node()->children[0]->frame_entry.get());
  EXPECT_EQ(frame_url_b, entry3->root_node()->children[0]->frame_entry->url());
  EXPECT_EQ(data_url,
            entry3->root_node()->children[0]->children[0]->frame_entry->url());
  EXPECT_EQ(entry3->root_node()
                ->children[0]
                ->frame_entry->committed_origin()
                ->GetTupleOrPrecursorTupleIfOpaque(),
            entry3->root_node()
                ->children[0]
                ->children[0]
                ->frame_entry->committed_origin()
                ->GetTupleOrPrecursorTupleIfOpaque());

  // 4. Navigate the nested iframe cross-site.
  GURL frame_url_c(embedded_test_server()->GetURL(
      "c.com", "/navigation_controller/simple_page_2.html"));
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0)->child_at(0));
    EXPECT_TRUE(
        NavigateToURLFromRenderer(root->child_at(0)->child_at(0), frame_url_c));
    capturer.Wait();
  }
  ASSERT_EQ(1U, root->child_count());
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ(frame_url_b, root->child_at(0)->current_url());
  EXPECT_EQ(frame_url_c, root->child_at(0)->child_at(0)->current_url());

  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(3, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry4 = controller.GetLastCommittedEntry();

  // The entry should have FrameNavigationEntries for the subframes.
  ASSERT_EQ(1U, entry4->root_node()->children.size());
  ASSERT_EQ(1U, entry4->root_node()->children[0]->children.size());
  EXPECT_EQ(entry3->root_node()->frame_entry.get(),
            entry4->root_node()->frame_entry.get());
  EXPECT_EQ(entry3->root_node()->children[0]->frame_entry.get(),
            entry4->root_node()->children[0]->frame_entry.get());
  EXPECT_NE(entry3->root_node()->children[0]->children[0]->frame_entry.get(),
            entry4->root_node()->children[0]->children[0]->frame_entry.get());
  EXPECT_EQ(frame_url_b, entry4->root_node()->children[0]->frame_entry->url());
  EXPECT_EQ(frame_url_c,
            entry4->root_node()->children[0]->children[0]->frame_entry->url());

  // Remember the DSNs for later.
  int64_t root_dsn =
      entry4->root_node()->frame_entry->document_sequence_number();
  int64_t frame_b_dsn =
      entry4->root_node()->children[0]->frame_entry->document_sequence_number();
  int64_t frame_c_dsn = entry4->root_node()
                            ->children[0]
                            ->children[0]
                            ->frame_entry->document_sequence_number();

  // 5. Navigate main frame cross-site, destroying the frames.
  GURL main_url_d(embedded_test_server()->GetURL(
      "d.com", "/navigation_controller/simple_page_2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url_d));
  ASSERT_EQ(0U, root->child_count());
  EXPECT_EQ(main_url_d, root->current_url());

  EXPECT_EQ(5, controller.GetEntryCount());
  EXPECT_EQ(4, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry5 = controller.GetLastCommittedEntry();
  EXPECT_EQ(0U, entry5->root_node()->children.size());

  // 6. Go back, recreating the iframe and its nested iframe.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }
  ASSERT_EQ(1U, root->child_count());
  ASSERT_EQ(1U, root->child_at(0)->child_count());
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ(frame_url_b, root->child_at(0)->current_url());
  EXPECT_EQ(frame_url_c, root->child_at(0)->child_at(0)->current_url());

  EXPECT_EQ(5, controller.GetEntryCount());
  EXPECT_EQ(3, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry4, controller.GetLastCommittedEntry());

  // The main frame should not have changed its DSN.
  EXPECT_EQ(root_dsn,
            entry4->root_node()->frame_entry->document_sequence_number());

  // The entry should have FrameNavigationEntries for the subframes.
  ASSERT_EQ(1U, entry4->root_node()->children.size());
  ASSERT_EQ(1U, entry4->root_node()->children[0]->children.size());
  EXPECT_EQ(frame_url_b, entry4->root_node()->children[0]->frame_entry->url());
  EXPECT_EQ(frame_url_c,
            entry4->root_node()->children[0]->children[0]->frame_entry->url());
  // The subframes should not have changed their DSNs.
  // See https://crbug.com/628286.
  EXPECT_EQ(frame_b_dsn, entry4->root_node()
                             ->children[0]
                             ->frame_entry->document_sequence_number());
  EXPECT_EQ(frame_c_dsn, entry4->root_node()
                             ->children[0]
                             ->children[0]
                             ->frame_entry->document_sequence_number());

  // Inject a JS value so that we can check for it later.
  EXPECT_TRUE(ExecJs(root, "foo=3;"));

  // 7. Go back again, to the data URL in the nested iframe.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }
  ASSERT_EQ(1U, root->child_count());
  ASSERT_EQ(1U, root->child_at(0)->child_count());
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ(frame_url_b, root->child_at(0)->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->child_at(0)->current_url());

  EXPECT_EQ(5, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry3, controller.GetLastCommittedEntry());

  // The entry should have FrameNavigationEntries for the subframes.
  ASSERT_EQ(1U, entry3->root_node()->children.size());
  ASSERT_EQ(1U, entry3->root_node()->children[0]->children.size());
  EXPECT_EQ(frame_url_b, entry3->root_node()->children[0]->frame_entry->url());
  EXPECT_EQ(data_url,
            entry3->root_node()->children[0]->children[0]->frame_entry->url());
  EXPECT_EQ(entry3->root_node()
                ->children[0]
                ->frame_entry->committed_origin()
                ->GetTupleOrPrecursorTupleIfOpaque(),
            entry3->root_node()
                ->children[0]
                ->children[0]
                ->frame_entry->committed_origin()
                ->GetTupleOrPrecursorTupleIfOpaque());

  // Verify that we did not reload the main frame. See https://crbug.com/586234.
  EXPECT_EQ(3, EvalJs(root, "foo"));

  // 8. Go back again, to the data URL in the first subframe.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }
  ASSERT_EQ(1U, root->child_count());
  ASSERT_EQ(0U, root->child_at(0)->child_count());
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->current_url());

  EXPECT_EQ(5, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry2, controller.GetLastCommittedEntry());

  // The entry should have a FrameNavigationEntry for the subframe.
  ASSERT_EQ(1U, entry2->root_node()->children.size());
  EXPECT_EQ(data_url, entry2->root_node()->children[0]->frame_entry->url());
  EXPECT_EQ(entry2->root_node()
                ->frame_entry->committed_origin()
                ->GetTupleOrPrecursorTupleIfOpaque(),
            entry2->root_node()
                ->children[0]
                ->frame_entry->committed_origin()
                ->GetTupleOrPrecursorTupleIfOpaque());
  ASSERT_TRUE(entry2->root_node()
                  ->children[0]
                  ->frame_entry->initiator_origin()
                  .has_value());
  EXPECT_EQ(url::Origin::Create(main_url_a),
            entry2->root_node()
                ->children[0]
                ->frame_entry->initiator_origin()
                .value());

  // 9. Go back again, to the initial main frame page.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }
  ASSERT_EQ(0U, root->child_count());
  EXPECT_EQ(initial_url, root->current_url());

  EXPECT_EQ(5, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry1, controller.GetLastCommittedEntry());
  EXPECT_EQ(0U, entry1->root_node()->children.size());

  // 10. Go forward multiple entries and verify the correct subframe URLs load.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoToOffset(2);
    back_load_observer.Wait();
  }
  ASSERT_EQ(1U, root->child_count());
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ(frame_url_b, root->child_at(0)->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->child_at(0)->current_url());

  EXPECT_EQ(5, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry3, controller.GetLastCommittedEntry());

  // The entry should have FrameNavigationEntries for the subframes.
  ASSERT_EQ(1U, entry3->root_node()->children.size());
  EXPECT_EQ(frame_url_b, entry3->root_node()->children[0]->frame_entry->url());
  EXPECT_EQ(data_url,
            entry3->root_node()->children[0]->children[0]->frame_entry->url());
  EXPECT_EQ(entry3->root_node()
                ->children[0]
                ->frame_entry->committed_origin()
                ->GetTupleOrPrecursorTupleIfOpaque(),
            entry3->root_node()
                ->children[0]
                ->children[0]
                ->frame_entry->committed_origin()
                ->GetTupleOrPrecursorTupleIfOpaque());
}

// Verify that we navigate to the fallback (original) URL if a subframe's
// FrameNavigationEntry can't be found during a history navigation.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_SubframeHistoryFallback) {
  // 1. Start on a page with a data URL iframe.
  GURL main_url_a(embedded_test_server()->GetURL(
      "a.com", "/navigation_controller/page_with_data_iframe.html"));
  GURL data_url("data:text/html,Subframe");
  EXPECT_TRUE(NavigateToURL(shell(), main_url_a));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(1U, root->child_count());
  ASSERT_EQ(0U, root->child_at(0)->child_count());
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->current_url());

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();

  // The entry should have a FrameNavigationEntry for the data subframe.
  ASSERT_EQ(1U, entry1->root_node()->children.size());
  EXPECT_EQ(data_url, entry1->root_node()->children[0]->frame_entry->url());

  // 2. Navigate the iframe cross-site.
  GURL frame_url_b(embedded_test_server()->GetURL(
      "b.com", "/navigation_controller/simple_page_1.html"));
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url_b));
    capturer.Wait();
  }
  ASSERT_EQ(1U, root->child_count());
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ(frame_url_b, root->child_at(0)->current_url());

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();

  // The entry should have a FrameNavigationEntry for the b.com subframe.
  ASSERT_EQ(1U, entry2->root_node()->children.size());
  EXPECT_EQ(entry1->root_node()->frame_entry.get(),
            entry2->root_node()->frame_entry.get());
  EXPECT_NE(entry1->root_node()->children[0]->frame_entry.get(),
            entry2->root_node()->children[0]->frame_entry.get());
  EXPECT_EQ(frame_url_b, entry2->root_node()->children[0]->frame_entry->url());

  // 3. Navigate main frame cross-site, destroying the frames.
  GURL main_url_c(embedded_test_server()->GetURL(
      "c.com", "/navigation_controller/simple_page_2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url_c));
  ASSERT_EQ(0U, root->child_count());
  EXPECT_EQ(main_url_c, root->current_url());

  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry3 = controller.GetLastCommittedEntry();
  EXPECT_EQ(0U, entry3->root_node()->children.size());

  // Force the subframe entry to have the wrong name, so that it isn't found
  // when we go back.
  entry2->root_node()->children[0]->frame_entry->set_frame_unique_name("wrong");

  // With BackForwardCache page is restored from cache instead of getting
  // recreated on history navigation, disable back/forward cache to force a
  // reload and a URL fetch.
  DisableBackForwardCacheForTesting(contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  // 4. Go back, recreating the iframe. The subframe entry won't be found, and
  // we should fall back to the default URL.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }
  ASSERT_EQ(1U, root->child_count());
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->current_url());

  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry2, controller.GetLastCommittedEntry());

  // The entry should have both the stale FrameNavigationEntry with the old
  // name and the new FrameNavigationEntry for the fallback navigation.
  ASSERT_EQ(2U, entry2->root_node()->children.size());
  EXPECT_EQ(frame_url_b, entry2->root_node()->children[0]->frame_entry->url());
  EXPECT_EQ(data_url, entry2->root_node()->children[1]->frame_entry->url());
}

// Allows waiting until an URL with a data scheme commits in any frame.
class DataUrlCommitObserver : public WebContentsObserver {
 public:
  explicit DataUrlCommitObserver(WebContents* web_contents)
      : WebContentsObserver(web_contents),
        message_loop_runner_(new MessageLoopRunner) {}

  void Wait() { message_loop_runner_->Run(); }

 private:
  void DidFinishNavigation(NavigationHandle* navigation_handle) override {
    if (navigation_handle->HasCommitted() &&
        !navigation_handle->IsErrorPage() &&
        navigation_handle->GetURL().scheme() == "data")
      message_loop_runner_->Quit();
  }

  // The MessageLoopRunner used to spin the message loop.
  scoped_refptr<MessageLoopRunner> message_loop_runner_;
};

// Verify that dynamically generated iframes load properly during a history
// navigation if no history item can be found for them.
// See https://crbug.com/649345.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_DynamicSubframeHistoryFallback) {
  // 1. Start on a page with a script-generated iframe.  The iframe has a
  // dynamic name, starts at about:blank, and gets navigated to a dynamic data
  // URL as the page is loading.
  GURL main_url_a(embedded_test_server()->GetURL(
      "a.com", "/navigation_controller/dynamic_iframe.html"));
  {
    // Wait until the data URL has committed, even if load stop happens after
    // about:blank load.
    DataUrlCommitObserver data_observer(shell()->web_contents());
    EXPECT_TRUE(NavigateToURL(shell(), main_url_a));
    data_observer.Wait();
  }
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(1U, root->child_count());
  ASSERT_EQ(0U, root->child_at(0)->child_count());
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ("data", root->child_at(0)->current_url().scheme());

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();

  // The entry should have a FrameNavigationEntry for the data subframe.
  ASSERT_EQ(1U, entry1->root_node()->children.size());
  EXPECT_EQ("data",
            entry1->root_node()->children[0]->frame_entry->url().scheme());

  // 2. Navigate main frame cross-site, destroying the frames.
  GURL main_url_b(embedded_test_server()->GetURL(
      "b.com", "/navigation_controller/simple_page_2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url_b));
  ASSERT_EQ(0U, root->child_count());
  EXPECT_EQ(main_url_b, root->current_url());

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();
  EXPECT_EQ(0U, entry2->root_node()->children.size());

  // With BackForwardCache page is restored from cache instead of getting
  // recreated on history navigation, disable back/forward cache to force a
  // reload and a URL fetch.
  DisableBackForwardCacheForTesting(contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  // 3. Go back, recreating the iframe.  The subframe will have a new name this
  // time, so we won't find a history item for it.  We should let the new data
  // URL be loaded into it, rather than clobbering it with an about:blank page.
  {
    // Wait until the data URL has committed, even if load stop happens first.
    DataUrlCommitObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  ASSERT_EQ(1U, root->child_count());
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ("data", root->child_at(0)->current_url().scheme());

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry1, controller.GetLastCommittedEntry());

  // There should be only 1 FNE, because when the child frame is dynamically
  // created or recreated from javascript, it's FNE will be removed when the
  // frame is removed.
  ASSERT_EQ(1U, entry1->root_node()->children.size());
  EXPECT_EQ("data",
            entry1->root_node()->children[0]->frame_entry->url().scheme());

  // The iframe commit should have been classified AUTO_SUBFRAME and not
  // NEW_SUBFRAME, so we should still be able to go forward.
  EXPECT_TRUE(shell()->web_contents()->GetController().CanGoForward());
}

// Verify that we don't clobber any content injected into the initial blank page
// if we go back to an about:blank subframe.  See https://crbug.com/626416.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_RecreatedBlankSubframe) {
  // 1. Start on a page that injects content into an about:blank iframe.
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/inject_into_blank_iframe.html"));
  GURL blank_url(url::kAboutBlankURL);
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(1U, root->child_count());
  ASSERT_EQ(0U, root->child_at(0)->child_count());
  EXPECT_EQ(main_url, root->current_url());
  EXPECT_EQ(blank_url, root->child_at(0)->current_url());

  // Verify that the parent was able to script the iframe.
  EXPECT_EQ("Injected text",
            EvalJs(root->child_at(0), "document.body.innerHTML"));

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();

  // The entry should have a FrameNavigationEntry for the blank subframe.
  ASSERT_EQ(1U, entry->root_node()->children.size());
  scoped_refptr<FrameNavigationEntry> child_frame_entry =
      entry->root_node()->children[0]->frame_entry.get();
  EXPECT_EQ(blank_url, child_frame_entry->url());

  // 2. Navigate the main frame, destroying the frames.
  GURL main_url_2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url_2));
  ASSERT_EQ(0U, root->child_count());
  EXPECT_EQ(main_url_2, root->current_url());

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

  // 3. Go back, recreating the iframe.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    controller.GoBack();
    back_load_observer.Wait();
  }
  ASSERT_EQ(1U, root->child_count());
  EXPECT_EQ(main_url, root->current_url());
  EXPECT_EQ(blank_url, root->child_at(0)->current_url());

  // Verify that the parent was able to script the iframe.
  EXPECT_EQ("Injected text",
            EvalJs(root->child_at(0), "document.body.innerHTML"));

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry, controller.GetLastCommittedEntry());

  // The entry should have the same FrameNavigationEntry for the blank subframe.
  ASSERT_EQ(1U, entry->root_node()->children.size());
  EXPECT_EQ(child_frame_entry,
            entry->root_node()->children[0]->frame_entry.get());
  EXPECT_EQ(blank_url, entry->root_node()->children[0]->frame_entry->url());
}

// Verify that we correctly load nested iframes injected into a page if we go
// back and recreate them.  Also confirm that form values are not restored for
// forms injected into about:blank pages.  See https://crbug.com/657896.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_RecreatedInjectedBlankSubframe) {
  // The test assumes the previous iframe gets deleted after navigation and
  // later recreated on history navigations. Disable back/forward cache to
  // ensure that it doesn't get preserved in the cache.
  DisableBackForwardCacheForTesting(shell()->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  // 1. Start on a page that injects a nested iframe into an injected
  // about:blank iframe.
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/inject_subframe_into_blank_iframe.html"));
  GURL blank_url(url::kAboutBlankURL);
  GURL inner_url(
      embedded_test_server()->GetURL("/navigation_controller/form.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Verify that the inner iframe was able to load.
  ASSERT_EQ(1U, root->child_count());
  ASSERT_EQ(1U, root->child_at(0)->child_count());
  ASSERT_EQ(0U, root->child_at(0)->child_at(0)->child_count());
  EXPECT_EQ(main_url, root->current_url());
  EXPECT_EQ(blank_url, root->child_at(0)->current_url());
  EXPECT_EQ(inner_url, root->child_at(0)->child_at(0)->current_url());

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();

  // The entry should have FrameNavigationEntries for the subframes.
  ASSERT_EQ(1U, entry->root_node()->children.size());
  scoped_refptr<FrameNavigationEntry> child_frame_entry =
      entry->root_node()->children[0]->frame_entry.get();
  EXPECT_EQ(blank_url, entry->root_node()->children[0]->frame_entry->url());
  EXPECT_EQ(inner_url,
            entry->root_node()->children[0]->children[0]->frame_entry->url());

  // Set a value in the form which will be stored in the PageState.
  EXPECT_TRUE(ExecJs(root->child_at(0)->child_at(0),
                     "document.getElementById('itext').value = 'modified';"));

  // 2. Navigate the main frame same-site, destroying the subframes.
  GURL main_url_2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url_2));
  ASSERT_EQ(0U, root->child_count());
  EXPECT_EQ(main_url_2, root->current_url());

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

  // 3. Go back, recreating the subframes.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    controller.GoBack();
    back_load_observer.Wait();
  }
  ASSERT_EQ(1U, root->child_count());
  EXPECT_EQ(main_url, root->current_url());
  EXPECT_EQ(blank_url, root->child_at(0)->current_url());

  // Verify that the inner iframe went to the correct URL.
  EXPECT_EQ(inner_url, root->child_at(0)->child_at(0)->current_url());

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry, controller.GetLastCommittedEntry());

  // There is only 1 child frame in the frame tree and only 1 FNE, because when
  // the child frame is dynamically created or recreated from javascript, it's
  // FNE will be removed when the frame is removed.
  ASSERT_EQ(1U, root->child_count());
  ASSERT_EQ(1U, entry->root_node()->children.size());

  // The entry should have FrameNavigationEntries for the subframes.
  EXPECT_NE(child_frame_entry,
            entry->root_node()->children[0]->frame_entry.get());
  EXPECT_EQ(blank_url, entry->root_node()->children[0]->frame_entry->url());
  EXPECT_EQ(inner_url,
            entry->root_node()->children[0]->children[0]->frame_entry->url());

  // With injected about:blank iframes, we never restore form values from
  // PageState.
  EXPECT_EQ("", EvalJs(root->child_at(0)->child_at(0),
                       "document.getElementById('itext').value"));
}

// Verify that we correctly load a nested iframe created by an injected iframe
// srcdoc if we go back and recreate the frames.
//
// This test is similar to
// NavigationControllerBrowserTest.
//     FrameNavigationEntry_RecreatedInjectedBlankSubframe
// and RenderFrameHostManagerTest.RestoreSubframeFileAccessForHistoryNavigation.
//
// This test worked before and after the fix for https://crbug.com/657896, but
// it failed with a preliminary version of the fix (see also
// https://crbug.com/657896#c9).
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_RecreatedInjectedSrcdocSubframe) {
  // The test assumes the previous iframe gets deleted after navigation and
  // later recreated on history navigations. Disable back/forward cache to
  // ensure that it doesn't get preserved in the cache.
  DisableBackForwardCacheForTesting(shell()->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);
  // 1. Start on a page that injects a nested iframe srcdoc which contains a
  // nested iframe.
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/inject_iframe_srcdoc_with_nested_frame.html"));
  GURL inner_url(
      embedded_test_server()->GetURL("/navigation_controller/form.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Verify that the inner iframe was able to load.
  ASSERT_EQ(1U, root->child_count());
  ASSERT_EQ(1U, root->child_at(0)->child_count());
  ASSERT_EQ(0U, root->child_at(0)->child_at(0)->child_count());
  EXPECT_EQ(main_url, root->current_url());
  EXPECT_TRUE(root->child_at(0)->current_url().IsAboutSrcdoc());
  EXPECT_EQ(inner_url, root->child_at(0)->child_at(0)->current_url());

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();

  // The entry should have FrameNavigationEntries for the subframes.
  ASSERT_EQ(1U, entry->root_node()->children.size());
  scoped_refptr<FrameNavigationEntry> child_frame_entry =
      entry->root_node()->children[0]->frame_entry.get();
  EXPECT_TRUE(
      entry->root_node()->children[0]->frame_entry->url().IsAboutSrcdoc());
  EXPECT_EQ(inner_url,
            entry->root_node()->children[0]->children[0]->frame_entry->url());

  // Set a value in the form which will be stored in the PageState.
  EXPECT_TRUE(ExecJs(root->child_at(0)->child_at(0),
                     "document.getElementById('itext').value = 'modified';"));

  // 2. Navigate the main frame same-site, destroying the subframes.
  GURL main_url_2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url_2));
  ASSERT_EQ(0U, root->child_count());
  EXPECT_EQ(main_url_2, root->current_url());

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

  // 3. Go back, recreating the subframes.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    controller.GoBack();
    back_load_observer.Wait();
  }
  ASSERT_EQ(1U, root->child_count());
  ASSERT_EQ(1U, root->child_at(0)->child_count());
  ASSERT_EQ(0U, root->child_at(0)->child_at(0)->child_count());
  EXPECT_EQ(main_url, root->current_url());
  EXPECT_TRUE(root->child_at(0)->current_url().IsAboutSrcdoc());

  // Verify that the inner iframe went to the correct URL.
  EXPECT_EQ(inner_url, root->child_at(0)->child_at(0)->current_url());

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry, controller.GetLastCommittedEntry());

  // There is only 1 child frame in the frame tree and only 1 FNE, because when
  // the child frame is dynamically created or recreated from javascript, it's
  // FNE will be removed when the frame is removed.
  ASSERT_EQ(1U, root->child_count());
  ASSERT_EQ(1U, entry->root_node()->children.size());

  // The entry should have FrameNavigationEntries for the subframes.
  EXPECT_NE(child_frame_entry,
            entry->root_node()->children[0]->frame_entry.get());
  EXPECT_TRUE(
      entry->root_node()->children[0]->frame_entry->url().IsAboutSrcdoc());
  EXPECT_EQ(inner_url,
            entry->root_node()->children[0]->children[0]->frame_entry->url());

  // With *injected* iframe srcdoc pages, we don't restore form values from
  // PageState (because iframes injected by javascript always get a fresh,
  // random unique name each time they are created or recreated - see
  // https://crbug.com/500260).
  //
  // Note that restoring form values in srcdoc frames created via static html is
  // expected to work and is tested by
  // RenderFrameHostManagerTest.RestoreSubframeFileAccessForHistoryNavigation.
  EXPECT_EQ("", EvalJs(root->child_at(0)->child_at(0),
                       "document.getElementById('itext').value"));
}

// Verify that we can load about:blank in an iframe when going back to a page,
// if that iframe did not originally have about:blank in it.  See
// https://crbug.com/657896.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_RecreatedSubframeToBlank) {
  // 1. Start on a page with a data iframe.
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_data_iframe.html"));
  GURL data_url("data:text/html,Subframe");
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(1U, root->child_count());
  ASSERT_EQ(0U, root->child_at(0)->child_count());
  EXPECT_EQ(main_url, root->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->current_url());

  // 2. Navigate the subframe to about:blank.
  GURL blank_url(url::kAboutBlankURL);
  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), blank_url));
  EXPECT_EQ(blank_url, root->child_at(0)->current_url());
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();

  // The entry should have a FrameNavigationEntry for the blank subframe.
  ASSERT_EQ(1U, entry->root_node()->children.size());
  scoped_refptr<FrameNavigationEntry> blank_entry =
      entry->root_node()->children[0]->frame_entry.get();
  EXPECT_EQ(blank_url, blank_entry->url());

  // 3. Navigate the main frame, destroying the frames.
  GURL main_url_2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url_2));
  ASSERT_EQ(0U, root->child_count());
  EXPECT_EQ(main_url_2, root->current_url());

  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());

  // 3. Go back, recreating the iframe.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    controller.GoBack();
    back_load_observer.Wait();
  }
  ASSERT_EQ(1U, root->child_count());
  EXPECT_EQ(main_url, root->current_url());
  EXPECT_EQ(blank_url, root->child_at(0)->current_url());

  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry, controller.GetLastCommittedEntry());

  // The entry should have a FrameNavigationEntry for the blank subframe.
  ASSERT_EQ(1U, entry->root_node()->children.size());
  EXPECT_EQ(blank_entry, entry->root_node()->children[0]->frame_entry.get());
  EXPECT_EQ(blank_url, entry->root_node()->children[0]->frame_entry->url());
}

// Ensure we don't crash if an onload handler removes an about:blank frame after
// recreating it on a back/forward.  See https://crbug.com/638166.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_RemoveRecreatedBlankSubframe) {
  // 1. Start on a page that removes its about:blank iframe during onload.
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/remove_blank_iframe_on_load.html"));
  GURL blank_url(url::kAboutBlankURL);
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_EQ(main_url, root->current_url());

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();

  // The entry should have a FrameNavigationEntry for the blank subframe, even
  // though it is being removed from the page.
  ASSERT_EQ(1U, entry->root_node()->children.size());
  EXPECT_EQ(blank_url, entry->root_node()->children[0]->frame_entry->url());

  // 2. Navigate the main frame, destroying the frames.
  GURL main_url_2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url_2));
  ASSERT_EQ(0U, root->child_count());
  EXPECT_EQ(main_url_2, root->current_url());

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

  // 3. Go back, recreating the iframe (and removing it again).
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    controller.GoBack();
    back_load_observer.Wait();
  }
  EXPECT_EQ(main_url, root->current_url());

  // Check that the renderer is still alive.
  EXPECT_TRUE(ExecJs(shell(), "console.log('Success');"));

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(entry, controller.GetLastCommittedEntry());

  // The entry should have a FrameNavigationEntry for the blank subframe.
  ASSERT_EQ(1U, entry->root_node()->children.size());
  EXPECT_EQ(blank_url, entry->root_node()->children[0]->frame_entry->url());
}

// Verifies that we clear the children FrameNavigationEntries if a history
// navigation redirects, so that we don't try to load previous history items in
// frames of the new page.  This should only clear the children of the frame
// that is redirecting.  See https://crbug.com/585194.
//
// Specifically, this test covers the following interesting cases:
// - Subframe redirect when going back from a different main frame (step 4).
// - Subframe redirect without changing the main frame (step 6).
// - Main frame redirect, clearing the children (step 8).
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_BackWithRedirect) {
  // The test assumes the previous iframe gets deleted after navigation and
  // later recreated on history navigations. Disable back/forward cache to
  // ensure that it doesn't get preserved in the cache.
  DisableBackForwardCacheForTesting(shell()->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  // 1. Start on a page with two frames.
  GURL initial_url(
      embedded_test_server()->GetURL("/frame_tree/page_with_two_frames.html"));
  EXPECT_TRUE(NavigateToURL(shell(), initial_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_EQ(initial_url, root->current_url());
  EXPECT_EQ(2U, root->child_count());
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();
  EXPECT_EQ(2U, entry1->root_node()->children.size());

  // 2. Navigate both iframes to a page with a nested iframe.
  GURL frame_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/page_with_data_iframe.html"));
  GURL data_url("data:text/html,Subframe");
  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url));
  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(1), frame_url));
  EXPECT_EQ(initial_url, root->current_url());
  EXPECT_EQ(frame_url, root->child_at(0)->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->child_at(0)->current_url());
  EXPECT_EQ(frame_url, root->child_at(1)->current_url());
  EXPECT_EQ(data_url, root->child_at(1)->child_at(0)->current_url());

  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();

  // Verify subframe entries.
  NavigationEntryImpl::TreeNode* root_node = entry2->root_node();
  ASSERT_EQ(2U, root_node->children.size());
  EXPECT_EQ(frame_url, root_node->children[0]->frame_entry->url());
  EXPECT_EQ(data_url, root_node->children[0]->children[0]->frame_entry->url());
  EXPECT_EQ(frame_url, root_node->children[1]->frame_entry->url());
  EXPECT_EQ(data_url, root_node->children[1]->children[0]->frame_entry->url());

  // Cause the first iframe to redirect when we come back later.  It will go
  // cross-site to a page with an about:blank iframe.
  GURL frame_redirect_dest_url(embedded_test_server()->GetURL(
      "bar.com", "/navigation_controller/page_with_iframe.html"));
  GURL blank_url(url::kAboutBlankURL);
  {
    TestNavigationObserver observer(shell()->web_contents());
    std::string script = "history.replaceState({}, '', '/server-redirect?" +
                         frame_redirect_dest_url.spec() + "')";
    EXPECT_TRUE(ExecJs(root->child_at(0), script));
    observer.Wait();
  }

  // We should not have lost subframe entries for the nested frame.
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  scoped_refptr<FrameNavigationEntry> nested_entry =
      entry2->GetFrameEntry(root->child_at(0)->child_at(0));
  EXPECT_TRUE(nested_entry);
  EXPECT_EQ(data_url, nested_entry->url());

  // 3. Navigate the main frame to a different page.  When we come back, we'll
  // commit the main frame first and have no pending entry when navigating the
  // subframes.
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url2));
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(3, controller.GetLastCommittedEntryIndex());

  // 4. Go back. The first iframe should redirect to a cross-site page with a
  // different nested iframe.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }
  EXPECT_EQ(initial_url, root->current_url());
  EXPECT_EQ(frame_redirect_dest_url, root->child_at(0)->current_url());
  EXPECT_EQ(blank_url, root->child_at(0)->child_at(0)->current_url());
  EXPECT_EQ(frame_url, root->child_at(1)->current_url());
  EXPECT_EQ(data_url, root->child_at(1)->child_at(0)->current_url());

  // Check the FrameNavigationEntries as well.
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(frame_redirect_dest_url,
            entry2->GetFrameEntry(root->child_at(0))->url());
  EXPECT_EQ(blank_url,
            entry2->GetFrameEntry(root->child_at(0)->child_at(0))->url());
  EXPECT_EQ(frame_url, entry2->GetFrameEntry(root->child_at(1))->url());
  EXPECT_EQ(data_url,
            entry2->GetFrameEntry(root->child_at(1)->child_at(0))->url());

  // Now cause the second iframe to redirect when we come back to it.
  {
    TestNavigationObserver observer(shell()->web_contents());
    std::string script = "history.replaceState({}, '', '/server-redirect?" +
                         frame_redirect_dest_url.spec() + "')";
    EXPECT_TRUE(ExecJs(root->child_at(1), script));
    observer.Wait();
  }

  // 5. Navigate the other iframe elsewhere, so that going back does not
  // require a navigation in the main frame.  This means there will be a
  // pending entry when the subframe commits, exercising a different path than
  // step 4.
  {
    FrameNavigateParamsCapturer capturer(root->child_at(1));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(1), url2));
    capturer.Wait();
  }
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(3, controller.GetLastCommittedEntryIndex());

  // 6. As in step 4, go back but redirect, resetting the children.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }
  EXPECT_EQ(initial_url, root->current_url());
  EXPECT_EQ(frame_redirect_dest_url, root->child_at(0)->current_url());
  EXPECT_EQ(blank_url, root->child_at(0)->child_at(0)->current_url());
  EXPECT_EQ(frame_redirect_dest_url, root->child_at(1)->current_url());
  EXPECT_EQ(blank_url, root->child_at(1)->child_at(0)->current_url());

  // Check the FrameNavigationEntries as well.
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(frame_redirect_dest_url,
            entry2->GetFrameEntry(root->child_at(0))->url());
  EXPECT_EQ(blank_url,
            entry2->GetFrameEntry(root->child_at(0)->child_at(0))->url());
  EXPECT_EQ(frame_redirect_dest_url,
            entry2->GetFrameEntry(root->child_at(1))->url());
  EXPECT_EQ(blank_url,
            entry2->GetFrameEntry(root->child_at(1)->child_at(0))->url());

  // Now cause the main frame to redirect to a page with no frames when we come
  // back to it.
  GURL redirect_dest_url(embedded_test_server()->GetURL(
      "bar.com", "/navigation_controller/simple_page_2.html"));
  {
    TestNavigationObserver observer(shell()->web_contents());
    std::string script = "history.replaceState({}, '', '/server-redirect?" +
                         redirect_dest_url.spec() + "')";
    EXPECT_TRUE(ExecJs(root, script));
    observer.Wait();
  }

  // 7. Navigate the main frame to a different page.
  EXPECT_TRUE(NavigateToURL(shell(), url2));
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(3, controller.GetLastCommittedEntryIndex());

  // 8. Go back, causing the main frame to redirect to a page with no frames.
  // All child items should be gone, and |entry2| is deleted and replaced with a
  // new entry.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }
  EXPECT_EQ(redirect_dest_url, root->current_url());
  EXPECT_EQ(0U, root->child_count());
  EXPECT_EQ(0U,
            controller.GetLastCommittedEntry()->root_node()->children.size());
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
}

// Similar to FrameNavigationEntry_BackWithRedirect but with same-origin frames.
// (This wasn't working initially).
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_SameOriginBackWithRedirect) {
  // The test assumes the previous iframe gets deleted after navigation and
  // later recreated on history navigations. Disable back/forward cache to
  // ensure that it doesn't get preserved in the cache.
  DisableBackForwardCacheForTesting(shell()->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  // 1. Start on a page with an iframe.
  GURL initial_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_data_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), initial_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_EQ(initial_url, root->current_url());
  EXPECT_EQ(1U, root->child_count());
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();
  EXPECT_EQ(1U, entry1->root_node()->children.size());

  // 2. Navigate the iframe to a page with a nested iframe.
  GURL frame_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_data_iframe.html"));
  GURL data_url("data:text/html,Subframe");
  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url));
  EXPECT_EQ(initial_url, root->current_url());
  EXPECT_EQ(frame_url, root->child_at(0)->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->child_at(0)->current_url());

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();

  // Verify subframe entries.
  NavigationEntryImpl::TreeNode* root_node = entry2->root_node();
  ASSERT_EQ(1U, root_node->children.size());
  EXPECT_EQ(frame_url, root_node->children[0]->frame_entry->url());
  EXPECT_EQ(data_url, root_node->children[0]->children[0]->frame_entry->url());

  // Cause the iframe to redirect when we come back later.  It will go
  // same-origin to a page with an about:blank iframe.
  GURL frame_redirect_dest_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe.html"));
  {
    TestNavigationObserver observer(shell()->web_contents());
    std::string script = "history.replaceState({}, '', '/server-redirect?" +
                         frame_redirect_dest_url.spec() + "')";
    EXPECT_TRUE(ExecJs(root->child_at(0), script));
    observer.Wait();
  }

  // We should not have lost subframe entries for the nested frame.
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  scoped_refptr<FrameNavigationEntry> nested_entry =
      entry2->GetFrameEntry(root->child_at(0)->child_at(0));
  EXPECT_TRUE(nested_entry);
  EXPECT_EQ(data_url, nested_entry->url());

  // 3. Navigate the main frame to a different page.  When we come back, we'll
  // commit the main frame first and have no pending entry when navigating the
  // subframes.
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url2));
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());

  // 4. Go back. The first iframe should redirect to a same-origin page with a
  // different nested iframe.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    controller.GoBack();
    back_load_observer.Wait();
  }
  GURL blank_url(url::kAboutBlankURL);
  EXPECT_EQ(initial_url, root->current_url());
  EXPECT_EQ(frame_redirect_dest_url, root->child_at(0)->current_url());
  EXPECT_EQ(blank_url, root->child_at(0)->child_at(0)->current_url());

  // Check the FrameNavigationEntries as well.
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(frame_redirect_dest_url,
            entry2->GetFrameEntry(root->child_at(0))->url());
  EXPECT_EQ(blank_url,
            entry2->GetFrameEntry(root->child_at(0)->child_at(0))->url());

  // Now cause the main frame to redirect to a page with no frames when we come
  // back to it.
  GURL redirect_dest_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  {
    TestNavigationObserver observer(shell()->web_contents());
    std::string script = "history.replaceState({}, '', '/server-redirect?" +
                         redirect_dest_url.spec() + "')";
    EXPECT_TRUE(ExecJs(root, script));
    observer.Wait();
  }

  // 5. Navigate the main frame to a different page.
  EXPECT_TRUE(NavigateToURL(shell(), url2));
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());

  // 6. Go back, causing the main frame to redirect to a page with no frames.
  // All child items should be gone.
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    controller.GoBack();
    back_load_observer.Wait();
  }
  EXPECT_EQ(redirect_dest_url, root->current_url());
  EXPECT_EQ(0U, root->child_count());
  EXPECT_EQ(0U, entry2->root_node()->children.size());
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
}

// Verify that subframes can be restored in a new NavigationController using the
// PageState of an existing NavigationEntry.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_RestoreViaPageState) {
  // 1. Start on a page with a data URL iframe.
  GURL main_url_a(embedded_test_server()->GetURL(
      "a.com", "/navigation_controller/page_with_data_iframe.html"));
  GURL data_url("data:text/html,Subframe");
  EXPECT_TRUE(NavigateToURL(shell(), main_url_a));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(1U, root->child_count());
  ASSERT_EQ(0U, root->child_at(0)->child_count());
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->current_url());

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();

  // The entry should have a FrameNavigationEntry for the data subframe.
  ASSERT_EQ(1U, entry1->root_node()->children.size());
  EXPECT_EQ(data_url, entry1->root_node()->children[0]->frame_entry->url());

  // 2. Navigate the iframe cross-site.
  GURL frame_url_b(embedded_test_server()->GetURL(
      "b.com", "/navigation_controller/simple_page_1.html"));
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url_b));
    capturer.Wait();
  }
  ASSERT_EQ(1U, root->child_count());
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ(frame_url_b, root->child_at(0)->current_url());

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();

  // The entry should have a FrameNavigationEntry for the b.com subframe.
  ASSERT_EQ(1U, entry2->root_node()->children.size());
  EXPECT_EQ(frame_url_b, entry2->root_node()->children[0]->frame_entry->url());

  // 3. Navigate main frame cross-site, destroying the frames.
  GURL main_url_c(embedded_test_server()->GetURL(
      "c.com", "/navigation_controller/simple_page_2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url_c));
  ASSERT_EQ(0U, root->child_count());
  EXPECT_EQ(main_url_c, root->current_url());

  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry3 = controller.GetLastCommittedEntry();
  EXPECT_EQ(0U, entry3->root_node()->children.size());

  // 4. Create a NavigationEntry with the same PageState as |entry2| and verify
  // it has the same FrameNavigationEntry structure.
  std::unique_ptr<NavigationEntryImpl> restored_entry =
      NavigationEntryImpl::FromNavigationEntry(
          NavigationController::CreateNavigationEntry(
              main_url_a, Referrer(), /* initiator_origin= */ absl::nullopt,
              /* initiator_base_url= */ absl::nullopt,
              ui::PAGE_TRANSITION_RELOAD, false, std::string(),
              controller.GetBrowserContext(),
              nullptr /* blob_url_loader_factory */));
  EXPECT_EQ(0U, restored_entry->root_node()->children.size());
  std::unique_ptr<NavigationEntryRestoreContextImpl> context =
      std::make_unique<NavigationEntryRestoreContextImpl>();
  restored_entry->SetPageState(entry2->GetPageState(), context.get());

  // The entry should have a FrameNavigationEntry for the b.com subframe.
  EXPECT_EQ(main_url_a, restored_entry->root_node()->frame_entry->url());
  ASSERT_EQ(1U, restored_entry->root_node()->children.size());
  EXPECT_EQ(frame_url_b,
            restored_entry->root_node()->children[0]->frame_entry->url());

  // 5. Restore the new entry in a new tab and verify the correct URLs load.
  std::vector<std::unique_ptr<NavigationEntry>> entries;
  entries.push_back(std::move(restored_entry));
  Shell* new_shell = Shell::CreateNewWindow(
      controller.GetBrowserContext(), GURL::EmptyGURL(), nullptr, gfx::Size());
  FrameTreeNode* new_root =
      static_cast<WebContentsImpl*>(new_shell->web_contents())
          ->GetPrimaryFrameTree()
          .root();
  NavigationControllerImpl& new_controller =
      static_cast<NavigationControllerImpl&>(
          new_shell->web_contents()->GetController());
  new_controller.Restore(entries.size() - 1, RestoreType::kRestored, &entries);
  ASSERT_EQ(0u, entries.size());
  {
    TestNavigationObserver restore_observer(new_shell->web_contents());
    new_controller.LoadIfNecessary();
    restore_observer.Wait();
  }
  ASSERT_EQ(1U, new_root->child_count());
  EXPECT_EQ(main_url_a, new_root->current_url());
  EXPECT_EQ(frame_url_b, new_root->child_at(0)->current_url());

  EXPECT_EQ(1, new_controller.GetEntryCount());
  EXPECT_EQ(0, new_controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* new_entry = new_controller.GetLastCommittedEntry();

  // The entry should have a FrameNavigationEntry for the b.com subframe.
  EXPECT_EQ(main_url_a, new_entry->root_node()->frame_entry->url());
  ASSERT_EQ(1U, new_entry->root_node()->children.size());
  EXPECT_EQ(frame_url_b,
            new_entry->root_node()->children[0]->frame_entry->url());
}

// Verify that we can finish loading a page on restore if the PageState is
// missing subframes.  See https://crbug.com/638088.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_RestoreViaPartialPageState) {
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/navigation_controller/inject_into_blank_iframe.html"));
  GURL blank_url(url::kAboutBlankURL);
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Create a NavigationEntry to restore, as if it had been loaded before.  The
  // page has an about:blank iframe and injects content into it, but the
  // PageState lacks any subframe history items.  This may happen during a
  // restore of a bad session or if the page has changed since the last visit.
  // Chrome should be robust to this and should be able to load the frame from
  // its default URL.
  std::unique_ptr<NavigationEntryImpl> restored_entry =
      NavigationEntryImpl::FromNavigationEntry(
          NavigationController::CreateNavigationEntry(
              main_url, Referrer(), absl::nullopt /* initiator_origin= */,
              /* initiator_base_url= */ absl::nullopt,
              ui::PAGE_TRANSITION_RELOAD, false, std::string(),
              controller.GetBrowserContext(),
              nullptr /* blob_url_loader_factory */));
  std::unique_ptr<NavigationEntryRestoreContextImpl> context =
      std::make_unique<NavigationEntryRestoreContextImpl>();
  restored_entry->SetPageState(blink::PageState::CreateFromURL(main_url),
                               context.get());
  EXPECT_EQ(0U, restored_entry->root_node()->children.size());

  // Restore the new entry in a new tab and verify the iframe loads and has
  // content injected into it.
  std::vector<std::unique_ptr<NavigationEntry>> entries;
  entries.push_back(std::move(restored_entry));
  controller.Restore(entries.size() - 1, RestoreType::kRestored, &entries);
  ASSERT_EQ(0u, entries.size());
  {
    TestNavigationObserver restore_observer(shell()->web_contents());
    controller.LoadIfNecessary();
    restore_observer.Wait();
  }
  ASSERT_EQ(1U, root->child_count());
  EXPECT_EQ(main_url, root->current_url());
  EXPECT_EQ(blank_url, root->child_at(0)->current_url());

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* new_entry = controller.GetLastCommittedEntry();

  // The entry should have a FrameNavigationEntry for the blank subframe.
  EXPECT_EQ(main_url, new_entry->root_node()->frame_entry->url());
  ASSERT_EQ(1U, new_entry->root_node()->children.size());
  EXPECT_EQ(blank_url, new_entry->root_node()->children[0]->frame_entry->url());

  // Verify that the parent was able to script the iframe.
  EXPECT_EQ("Injected text",
            EvalJs(root->child_at(0), "document.body.innerHTML"));
}

// Verify that calling Restore() and LoadIfNecessary() on a NavigationController
// that already has existing entries & non-null pending entry won't crash and
// won't clear existing non-initial entries. This can happen because WebView's
// restoreState() API, which triggers the session restore code, can be called at
// any point in time, which used to cause crashes.
// Regression test for https://crbug.com/1287624.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RestoreAndLoadOnUsedNavigationController) {
  GURL url_1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  GURL url_3(embedded_test_server()->GetURL("a.com", "/title3.html"));

  // 1. Start in a tab with 1 NavigationEntry.
  EXPECT_TRUE(NavigateToURL(shell(), url_1));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_EQ(url_1, root->current_url());

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();

  // 2. Create a NavigationEntry with the same PageState as |entry1|.
  std::unique_ptr<NavigationEntryImpl> restored_entry =
      NavigationEntryImpl::FromNavigationEntry(
          NavigationController::CreateNavigationEntry(
              url_1, Referrer(), absl::nullopt /* initiator_origin= */,
              /* initiator_base_url= */ absl::nullopt,
              ui::PAGE_TRANSITION_RELOAD, false, std::string(),
              controller.GetBrowserContext(),
              nullptr /* blob_url_loader_factory */));
  EXPECT_EQ(0U, restored_entry->root_node()->children.size());
  std::unique_ptr<NavigationEntryRestoreContextImpl> context =
      std::make_unique<NavigationEntryRestoreContextImpl>();
  restored_entry->SetPageState(entry1->GetPageState(), context.get());

  // 3. Create a new tab with a new NavigationController, and navigate it twice.
  Shell* new_shell = Shell::CreateNewWindow(
      controller.GetBrowserContext(), GURL::EmptyGURL(), nullptr, gfx::Size());
  FrameTreeNode* new_root =
      static_cast<WebContentsImpl*>(new_shell->web_contents())
          ->GetPrimaryFrameTree()
          .root();
  NavigationControllerImpl& new_controller =
      static_cast<NavigationControllerImpl&>(
          new_shell->web_contents()->GetController());
  EXPECT_TRUE(NavigateToURL(new_shell, url_2));
  EXPECT_EQ(1, new_controller.GetEntryCount());
  EXPECT_EQ(0, new_controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry2 = new_controller.GetLastCommittedEntry();
  EXPECT_EQ(url_2, entry2->GetURL());

  EXPECT_TRUE(NavigateToURL(new_shell, url_3));
  EXPECT_EQ(2, new_controller.GetEntryCount());
  EXPECT_EQ(1, new_controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_3, new_controller.GetLastCommittedEntry()->GetURL());

  // 4. Start and pause a back navigation in the new NavigationController, which
  // will set the pending entry to `entry2`. Disable BFCache so that we can
  // pause the navigation.
  DisableBackForwardCacheForTesting(new_shell->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);
  TestNavigationManager back_navigation_manager(new_shell->web_contents(),
                                                url_2);
  new_controller.GoBack();
  EXPECT_TRUE(back_navigation_manager.WaitForRequestStart());
  EXPECT_EQ(entry2, new_controller.GetPendingEntry());
  EXPECT_TRUE(new_root->navigation_request());

  // 5. Restore `restored_entry` in the new tab.
  std::vector<std::unique_ptr<NavigationEntry>> entries;
  entries.push_back(std::move(restored_entry));
  new_controller.Restore(entries.size() - 1, RestoreType::kRestored, &entries);
  // The previously existing entries were kept, in addition to the restored
  // entries. Note that the last committed entry index is updated to 0, which
  // is supposed to point to `url_1`'s entry, but actually points to `url_2`'s
  // entry because the pre-existing entries are not cleared.
  // TODO(https://crbug.com/1287624): Fix this.
  EXPECT_EQ(3, new_controller.GetEntryCount());
  EXPECT_EQ(0, new_controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_2, new_controller.GetEntryAtIndex(0)->GetURL());
  EXPECT_EQ(url_3, new_controller.GetEntryAtIndex(1)->GetURL());
  EXPECT_EQ(url_1, new_controller.GetEntryAtIndex(2)->GetURL());
  // The pending entry is not cleared and the back navigation is still pending.
  EXPECT_EQ(entry2, new_controller.GetPendingEntry());
  EXPECT_TRUE(new_root->navigation_request());

  // 6. Call LoadIfNecessary() in the new tab.
  ASSERT_EQ(0u, entries.size());
  {
    TestNavigationObserver restore_observer(new_shell->web_contents());
    new_controller.LoadIfNecessary();
    restore_observer.Wait();
  }
  // Because the pending entry still pointed to the back navigation's entry,
  // `url_2` is loaded instead of the entry restored from the other tab.
  // TODO(https://crbug.com/1287624): This should load the restored entry
  // instead.
  EXPECT_EQ(3, new_controller.GetEntryCount());
  EXPECT_EQ(0, new_controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* current_entry = new_controller.GetLastCommittedEntry();
  EXPECT_EQ(entry2, current_entry);
  EXPECT_EQ(url_2, current_entry->GetURL());
  EXPECT_EQ(url_2, new_root->current_url());
}

// Verify that calling Restore with an empty set of entries does not crash,
// since this may be possible via Android WebView's restoreState() API.
// See https://crbug.com/1287624.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RestoreEmptyAndLoadOnUsedNavigationController) {
  GURL url_1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_2(embedded_test_server()->GetURL("a.com", "/title2.html"));

  // 1. Create a new tab with a new NavigationController, and navigate it twice.
  Shell* new_shell =
      Shell::CreateNewWindow(shell()->web_contents()->GetBrowserContext(),
                             GURL::EmptyGURL(), nullptr, gfx::Size());
  FrameTreeNode* new_root =
      static_cast<WebContentsImpl*>(new_shell->web_contents())
          ->GetPrimaryFrameTree()
          .root();
  NavigationControllerImpl& new_controller =
      static_cast<NavigationControllerImpl&>(
          new_shell->web_contents()->GetController());
  EXPECT_TRUE(NavigateToURL(new_shell, url_1));
  EXPECT_EQ(1, new_controller.GetEntryCount());
  EXPECT_EQ(0, new_controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry1 = new_controller.GetLastCommittedEntry();
  EXPECT_EQ(url_1, entry1->GetURL());

  EXPECT_TRUE(NavigateToURL(new_shell, url_2));
  EXPECT_EQ(2, new_controller.GetEntryCount());
  EXPECT_EQ(1, new_controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_2, new_controller.GetLastCommittedEntry()->GetURL());

  // 2. Start and pause a back navigation in the new NavigationController, which
  // will set the pending entry to `entry1`. Disable BFCache so that we can
  // pause the navigation.
  DisableBackForwardCacheForTesting(new_shell->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);
  TestNavigationManager back_navigation_manager(new_shell->web_contents(),
                                                url_1);
  new_controller.GoBack();
  EXPECT_TRUE(back_navigation_manager.WaitForRequestStart());
  EXPECT_EQ(entry1, new_controller.GetPendingEntry());
  EXPECT_TRUE(new_root->navigation_request());

  // 3. Restore an empty set of entries in the new tab, which is possible via
  // Android WebView's restoreState API.
  std::vector<std::unique_ptr<NavigationEntry>> entries;
  new_controller.Restore(-1, RestoreType::kRestored, &entries);
  // The previously existing entries were kept without crashing, and no changes
  // to the entries were made except the last committed index.
  // TODO(https://crbug.com/1287624): A selected index of -1 should not be
  // allowed.
  EXPECT_EQ(2, new_controller.GetEntryCount());

  // There is always a valid index.
  EXPECT_EQ(0, new_controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_1, new_controller.GetEntryAtIndex(0)->GetURL());
  EXPECT_EQ(url_2, new_controller.GetEntryAtIndex(1)->GetURL());

  // The pending entry is not cleared and the back navigation is still pending.
  EXPECT_EQ(entry1, new_controller.GetPendingEntry());
  EXPECT_TRUE(new_root->navigation_request());

  // 4. Call LoadIfNecessary() in the new tab.
  ASSERT_EQ(0u, entries.size());
  {
    TestNavigationObserver restore_observer(new_shell->web_contents());
    new_controller.LoadIfNecessary();
    restore_observer.Wait();
  }
  // Because the pending entry still pointed to the back navigation's entry,
  // `url_1` is loaded.
  // TODO(https://crbug.com/1287624): A selected index of -1 should not be
  // allowed.
  EXPECT_EQ(2, new_controller.GetEntryCount());
  EXPECT_EQ(0, new_controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* current_entry = new_controller.GetLastCommittedEntry();
  EXPECT_EQ(url_1, current_entry->GetURL());
  EXPECT_EQ(url_1, new_root->current_url());
}

// Verify that calling Restore with an entry that has an empty URL on a
// fresh/unused NavigationController does not crash.
// See also https://crbug.com/1240138.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RestoreEmptyURLEntryAndLoadOnFreshNavigationController) {
  // 1. Start in a tab and navigate it to about:blank.
  GURL about_blank_url(url::kAboutBlankURL);
  EXPECT_TRUE(NavigateToURL(shell(), about_blank_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_EQ(about_blank_url, root->current_url());

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();

  // 2. Create a NavigationEntry with the same PageState as |entry1| but with
  // an empty URL.
  std::unique_ptr<NavigationEntryImpl> restored_entry =
      NavigationEntryImpl::FromNavigationEntry(
          NavigationController::CreateNavigationEntry(
              GURL(), Referrer(), absl::nullopt /* initiator_origin= */,
              /* initiator_base_url= */ absl::nullopt,
              ui::PAGE_TRANSITION_RELOAD, false, std::string(),
              controller.GetBrowserContext(),
              nullptr /* blob_url_loader_factory */));
  EXPECT_EQ(0U, restored_entry->root_node()->children.size());
  std::unique_ptr<NavigationEntryRestoreContextImpl> context =
      std::make_unique<NavigationEntryRestoreContextImpl>();
  entry1->SetURL(GURL());
  restored_entry->SetPageState(entry1->GetPageState(), context.get());

  // 3. Restore the empty URL entry in a new tab.
  Shell* new_shell = Shell::CreateNewWindow(
      controller.GetBrowserContext(), GURL::EmptyGURL(), nullptr, gfx::Size());
  NavigationControllerImpl& new_controller =
      static_cast<NavigationControllerImpl&>(
          new_shell->web_contents()->GetController());
  std::vector<std::unique_ptr<NavigationEntry>> entries;
  entries.push_back(std::move(restored_entry));
  new_controller.Restore(0, RestoreType::kRestored, &entries);
  EXPECT_EQ(0u, entries.size());

  // The restored entry has its URL set to about:blank instead of an empty URL.
  EXPECT_EQ(1, new_controller.GetEntryCount());
  EXPECT_EQ(0, new_controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(about_blank_url, new_controller.GetLastCommittedEntry()->GetURL());

  // 4. Call LoadIfNecessary() in the new tab.
  {
    TestNavigationObserver restore_observer(new_shell->web_contents());
    new_controller.LoadIfNecessary();
    restore_observer.Wait();
  }

  // After loading, the restored entry's URL stays as "about:blank".
  EXPECT_EQ(1, new_controller.GetEntryCount());
  EXPECT_EQ(0, new_controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(about_blank_url, new_controller.GetLastCommittedEntry()->GetURL());
}

// Verify that the restore state of a pending entry doesn't change if an
// unrelated subframe commits while the navigation is in progress.
// Regression test for https://crbug.com/923423.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RestoreAndAddSubframeDuringPendingBack) {
  GURL url_1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_2(embedded_test_server()->GetURL("a.com", "/title2.html"));

  // 1. Start in a tab with 2 NavigationEntries.
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_TRUE(NavigateToURL(shell(), url_1));
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();
  EXPECT_TRUE(NavigateToURL(shell(), url_2));
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_2, root->current_url());

  // 2. Create NavigationEntries with the same PageState as the current entries.
  std::unique_ptr<NavigationEntryRestoreContextImpl> context =
      std::make_unique<NavigationEntryRestoreContextImpl>();
  std::unique_ptr<NavigationEntryImpl> restored_entry1 =
      NavigationEntryImpl::FromNavigationEntry(
          NavigationController::CreateNavigationEntry(
              url_1, Referrer(), absl::nullopt /* initiator_origin= */,
              /* initiator_base_url= */ absl::nullopt,
              ui::PAGE_TRANSITION_RELOAD, false, std::string(),
              controller.GetBrowserContext(),
              nullptr /* blob_url_loader_factory */));
  restored_entry1->SetPageState(entry1->GetPageState(), context.get());
  std::unique_ptr<NavigationEntryImpl> restored_entry2 =
      NavigationEntryImpl::FromNavigationEntry(
          NavigationController::CreateNavigationEntry(
              url_2, Referrer(), absl::nullopt /* initiator_origin= */,
              /* initiator_base_url= */ absl::nullopt,
              ui::PAGE_TRANSITION_RELOAD, false, std::string(),
              controller.GetBrowserContext(),
              nullptr /* blob_url_loader_factory */));
  restored_entry2->SetPageState(entry2->GetPageState(), context.get());

  // 3. Create a new tab and restore the entries.
  Shell* new_shell = Shell::CreateNewWindow(
      controller.GetBrowserContext(), GURL::EmptyGURL(), nullptr, gfx::Size());
  WebContentsImpl* new_web_contents =
      static_cast<WebContentsImpl*>(new_shell->web_contents());
  NavigationControllerImpl& new_controller =
      static_cast<NavigationControllerImpl&>(new_web_contents->GetController());
  std::vector<std::unique_ptr<NavigationEntry>> entries;
  entries.push_back(std::move(restored_entry1));
  entries.push_back(std::move(restored_entry2));
  new_controller.Restore(entries.size() - 1, RestoreType::kRestored, &entries);
  {
    TestNavigationObserver restore_observer(new_shell->web_contents());
    new_controller.LoadIfNecessary();
    restore_observer.Wait();
  }
  NavigationEntryImpl* new_entry1 = new_controller.GetEntryAtIndex(0);
  NavigationEntryImpl* new_entry2 = new_controller.GetEntryAtIndex(1);
  EXPECT_EQ(new_entry2, new_controller.GetLastCommittedEntry());

  // 4. Start and pause a back navigation in the new NavigationController, which
  // will set the pending entry to `new_entry1`.
  TestNavigationManager back_navigation_manager(new_shell->web_contents(),
                                                url_1);
  new_controller.GoBack();
  EXPECT_TRUE(back_navigation_manager.WaitForRequestStart());
  EXPECT_EQ(new_entry1, new_controller.GetPendingEntry());
  EXPECT_TRUE(new_entry1->IsRestored());

  // While waiting for the back navigation, add a new subframe and wait for it
  // to commit. This should not change the restore status of new_entry1, which
  // used to fail in https://crbug.com/923423 and caused a DCHECK crash. Note
  // that passing true for `wait_for_navigation` does not work because it waits
  // for load stop, which will not happen while the back navigation is pending.
  TestNavigationObserver subframe_observer(new_web_contents);
  CreateSubframe(new_web_contents, "child", url_2,
                 false /* wait_for_navigation */);
  subframe_observer.WaitForNavigationFinished();
  EXPECT_EQ(new_entry2, new_controller.GetLastCommittedEntry());
  EXPECT_EQ(new_entry1, new_controller.GetPendingEntry());
  EXPECT_TRUE(new_entry1->IsRestored());

  // Allow the back navigation to complete, clearing the restore status.
  ASSERT_TRUE(back_navigation_manager.WaitForNavigationFinished());
  EXPECT_FALSE(new_entry1->IsRestored());
  EXPECT_EQ(new_entry1, new_controller.GetLastCommittedEntry());
  EXPECT_EQ(2, new_controller.GetEntryCount());
  EXPECT_EQ(0, new_controller.GetLastCommittedEntryIndex());
}

// Verifies that the |frame_unique_name| is set to the correct frame, so that we
// can match subframe FrameNavigationEntries to newly created frames after
// back/forward and restore.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_FrameUniqueName) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // 1. Navigate the main frame.
  GURL url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_links.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  SiteInstanceImpl* main_site_instance =
      root->current_frame_host()->GetSiteInstance();

  // The main frame defaults to an empty name.
  scoped_refptr<FrameNavigationEntry> frame_entry =
      controller.GetLastCommittedEntry()->GetFrameEntry(root);
  EXPECT_EQ("", frame_entry->frame_unique_name());

  // 2. Add an unnamed subframe, which does an AUTO_SUBFRAME navigation.
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // The root FrameNavigationEntry hasn't changed.
  EXPECT_EQ(frame_entry,
            controller.GetLastCommittedEntry()->GetFrameEntry(root));

  // The subframe should have a generated name.
  FrameTreeNode* subframe = root->child_at(0);
  EXPECT_EQ(main_site_instance,
            subframe->current_frame_host()->GetSiteInstance());
  scoped_refptr<FrameNavigationEntry> subframe_entry =
      controller.GetLastCommittedEntry()->GetFrameEntry(subframe);
  EXPECT_THAT(subframe_entry->frame_unique_name(),
              testing::HasSubstr("dynamicFrame"));

  // 3. Add a named subframe.
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    std::string script = JsReplace(
        "var iframe = document.createElement('iframe');"
        "iframe.src = $1;"
        "iframe.name = 'foo';"
        "document.body.appendChild(iframe);",
        url);
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // The new subframe should have the specified name.
  EXPECT_EQ(frame_entry,
            controller.GetLastCommittedEntry()->GetFrameEntry(root));
  FrameTreeNode* foo_subframe = root->child_at(1);
  EXPECT_EQ(main_site_instance,
            foo_subframe->current_frame_host()->GetSiteInstance());
  scoped_refptr<FrameNavigationEntry> foo_subframe_entry =
      controller.GetLastCommittedEntry()->GetFrameEntry(foo_subframe);
  EXPECT_THAT(foo_subframe_entry->frame_unique_name(),
              testing::HasSubstr("dynamicFrame"));

  // 4. Navigating in the subframes cross-process shouldn't change their names.
  // TODO(creis): Fix the unnamed case in https://crbug.com/502317.
  GURL bar_url(embedded_test_server()->GetURL(
      "bar.com", "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURLFromRenderer(foo_subframe, bar_url));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  if (AreStrictSiteInstancesEnabled()) {
    EXPECT_NE(main_site_instance,
              foo_subframe->current_frame_host()->GetSiteInstance());
  } else {
    // When strict SiteInstances are not being used, the subframe should be
    // the same as its parent because both sites get routed to the default
    // SiteInstance.
    EXPECT_TRUE(main_site_instance->IsDefaultSiteInstance());
    EXPECT_EQ(main_site_instance,
              foo_subframe->current_frame_host()->GetSiteInstance());
  }

  foo_subframe_entry =
      controller.GetLastCommittedEntry()->GetFrameEntry(foo_subframe);
  EXPECT_THAT(foo_subframe_entry->frame_unique_name(),
              testing::HasSubstr("dynamicFrame"));
}

// Verify that navigations caused by client-side redirects populates the entry's
// replaced data.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ReplacedNavigationEntryData_ClientSideRedirect) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/client_redirect.html"));
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));

  {
    TestNavigationManager navigation_manager_1(shell()->web_contents(), url1);
    TestNavigationManager navigation_manager_2(shell()->web_contents(), url2);

    shell()->LoadURL(url1);

    ASSERT_TRUE(navigation_manager_1
                    .WaitForNavigationFinished());  // Initial navigation.
    ASSERT_TRUE(navigation_manager_2
                    .WaitForNavigationFinished());  // Client-side redirect.

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    ASSERT_EQ(url2, entry1->GetURL());
    ASSERT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry1->GetTransitionType(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
                                  ui::PAGE_TRANSITION_CLIENT_REDIRECT)));

    ASSERT_TRUE(entry1->GetReplacedEntryData().has_value());
    EXPECT_EQ(url1, entry1->GetReplacedEntryData()->first_committed_url);
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry1->GetReplacedEntryData()->first_transition_type,
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)))
        << base::StringPrintf(
               "%X", entry1->GetReplacedEntryData()->first_transition_type);
  }
}

// Verify that navigations caused by location.replace() populates the entry's
// replaced data.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ReplacedNavigationEntryData_LocationReplace) {
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));

  // Test fixture: start with typing a URL.
  {
    ASSERT_TRUE(NavigateToURL(shell(), url1));
    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    ASSERT_EQ(url1, entry1->GetURL());
    ASSERT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry1->GetTransitionType(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_FALSE(entry1->GetReplacedEntryData().has_value());
  }

  const base::Time time1 = controller.GetEntryAtIndex(0)->GetTimestamp();

  {
    // location.replace().
    FrameNavigateParamsCapturer capturer(root);
    std::string script = "location.replace('" + url2.spec() + "')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    ASSERT_EQ(url2, entry1->GetURL());
    ASSERT_NE(time1, entry1->GetTimestamp());

    ASSERT_TRUE(entry1->GetReplacedEntryData().has_value());
    EXPECT_EQ(url1, entry1->GetReplacedEntryData()->first_committed_url);
    EXPECT_EQ(time1, entry1->GetReplacedEntryData()->first_timestamp);
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry1->GetReplacedEntryData()->first_transition_type,
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
  }
}

// Verify that history.replaceState() populates the navigation entry's replaced
// entry data.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ReplacedNavigationEntryData_ReplaceState) {
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  GURL url3(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_3.html"));

  // Test fixture: start with typing a URL.
  {
    ASSERT_TRUE(NavigateToURL(shell(), url1));
    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    ASSERT_EQ(url1, entry1->GetURL());
    ASSERT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry1->GetTransitionType(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_FALSE(entry1->GetReplacedEntryData().has_value());
  }

  const base::Time time1 = controller.GetEntryAtIndex(0)->GetTimestamp();

  {
    // history.replaceState().
    FrameNavigateParamsCapturer capturer(root);
    std::string script =
        "history.replaceState({}, 'page 2', 'simple_page_2.html')";
    ASSERT_TRUE(ExecJs(root, script));
    capturer.Wait();

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    ASSERT_EQ(url2, entry1->GetURL());
    ASSERT_NE(time1, entry1->GetTimestamp());

    ASSERT_TRUE(entry1->GetReplacedEntryData().has_value());
    EXPECT_EQ(url1, entry1->GetReplacedEntryData()->first_committed_url);
    EXPECT_EQ(time1, entry1->GetReplacedEntryData()->first_timestamp);
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry1->GetReplacedEntryData()->first_transition_type,
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
  }

  {
    // Reload from the renderer side and make sure the replaced entry data
    // doesn't change.
    FrameNavigateParamsCapturer capturer(root);
    ASSERT_TRUE(ExecJs(root, "location.reload()"));
    capturer.Wait();

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    ASSERT_EQ(url2, entry1->GetURL());
    ASSERT_NE(time1, entry1->GetTimestamp());

    ASSERT_TRUE(entry1->GetReplacedEntryData().has_value());
    EXPECT_EQ(url1, entry1->GetReplacedEntryData()->first_committed_url);
    EXPECT_EQ(time1, entry1->GetReplacedEntryData()->first_timestamp);
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry1->GetReplacedEntryData()->first_transition_type,
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
  }

  {
    // history.replaceState().
    FrameNavigateParamsCapturer capturer(root);
    std::string script =
        "history.replaceState({}, 'page 3', 'simple_page_3.html')";
    ASSERT_TRUE(ExecJs(root, script));
    capturer.Wait();

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    ASSERT_EQ(url3, entry1->GetURL());
    ASSERT_NE(time1, entry1->GetTimestamp());

    ASSERT_TRUE(entry1->GetReplacedEntryData().has_value());
    EXPECT_EQ(url1, entry1->GetReplacedEntryData()->first_committed_url);
    EXPECT_EQ(time1, entry1->GetReplacedEntryData()->first_timestamp);
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry1->GetReplacedEntryData()->first_transition_type,
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
  }
}

// Verify that history.pushState() does not populate the navigation entry's
// replaced entry data.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ReplacedNavigationEntryData_PushState) {
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));

  // Test fixture: start with typing a URL.
  {
    ASSERT_TRUE(NavigateToURL(shell(), url1));
    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    ASSERT_EQ(url1, entry1->GetURL());
    ASSERT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry1->GetTransitionType(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_FALSE(entry1->GetReplacedEntryData().has_value());
  }

  {
    // history.pushState().
    FrameNavigateParamsCapturer capturer(root);
    std::string script =
        "history.pushState({}, 'page 2', 'simple_page_2.html')";
    ASSERT_TRUE(ExecJs(root, script));
    capturer.Wait();

    ASSERT_EQ(2, controller.GetEntryCount());
    ASSERT_EQ(url1, controller.GetEntryAtIndex(0)->GetURL());
    ASSERT_EQ(url2, controller.GetEntryAtIndex(1)->GetURL());

    EXPECT_FALSE(
        controller.GetEntryAtIndex(0)->GetReplacedEntryData().has_value());
    EXPECT_FALSE(
        controller.GetEntryAtIndex(1)->GetReplacedEntryData().has_value());
  }
}

// Verify that location.reload() does not populate the navigation entry's
// replaced entry data.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ReplacedNavigationEntryData_LocationReload) {
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));

  // Test fixture: start with typing a URL.
  {
    ASSERT_TRUE(NavigateToURL(shell(), url1));
    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    ASSERT_EQ(url1, entry1->GetURL());
    ASSERT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry1->GetTransitionType(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_FALSE(entry1->GetReplacedEntryData().has_value());
  }

  const base::Time time1 = controller.GetEntryAtIndex(0)->GetTimestamp();

  {
    // Reload from the renderer side and make sure replaced entry data is not
    // stored.
    FrameNavigateParamsCapturer capturer(root);
    ASSERT_TRUE(ExecJs(root, "location.reload()"));
    capturer.Wait();

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    ASSERT_EQ(url1, entry1->GetURL());
    ASSERT_NE(time1, entry1->GetTimestamp());

    // At least the timestamp has changed, so we need to keep a copy of the
    // replaced data.
    ASSERT_TRUE(entry1->GetReplacedEntryData().has_value());
    EXPECT_EQ(url1, entry1->GetReplacedEntryData()->first_committed_url);
    EXPECT_EQ(time1, entry1->GetReplacedEntryData()->first_timestamp);
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry1->GetReplacedEntryData()->first_transition_type,
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
  }
}

// Verify the scenario where the user goes back to a navigatin entry that had
// previously replaced it's URL (via history.replaceState()), for a URL that
// (if fetched) causes a server-side redirect. In this scenario, the fact of
// going back should not influence the replaced data, and hence the first URL
// prior to history.replaceState() should remain set.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    ReplacedNavigationEntryData_BackAfterReplaceStateWithRedirect) {
  // The test assumes the previous iframe gets deleted after navigation and
  // later recreated on history navigations. Disable back/forward cache to
  // ensure that it doesn't get preserved in the cache.
  DisableBackForwardCacheForTesting(shell()->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  GURL redirecting_url_to_url2(
      embedded_test_server()->GetURL("/server-redirect?" + url2.spec()));
  GURL url3(embedded_test_server()->GetURL("/simple_page.html"));

  // Start with typing a URL.
  {
    ASSERT_TRUE(NavigateToURL(shell(), url1));
    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    ASSERT_EQ(url1, entry1->GetURL());
    ASSERT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry1->GetTransitionType(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_FALSE(entry1->GetReplacedEntryData().has_value());
  }

  {
    // history.replaceState(), pointing to a URL that would redirect to |url2|.
    FrameNavigateParamsCapturer capturer(root);
    std::string script = "history.replaceState({}, 'page 2', '" +
                         redirecting_url_to_url2.spec() + "')";

    ASSERT_TRUE(ExecJs(root, script));
    capturer.Wait();

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    ASSERT_EQ(redirecting_url_to_url2, entry1->GetURL());
    ASSERT_TRUE(entry1->GetReplacedEntryData().has_value());
    ASSERT_EQ(url1, entry1->GetReplacedEntryData()->first_committed_url);
  }

  // Type another URL, |url3|.
  {
    ASSERT_TRUE(NavigateToURL(shell(), url3));
    ASSERT_EQ(2, controller.GetEntryCount());
  }

  // Back, which should redirect to |url2|.
  {
    FrameNavigateParamsCapturer capturer(root);
    shell()->web_contents()->GetController().GoBack();
    capturer.Wait();

    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    ASSERT_EQ(url2, entry1->GetURL());

    // We still expect |url1| in the replaced data.
    ASSERT_TRUE(entry1->GetReplacedEntryData().has_value());
    EXPECT_EQ(url1, entry1->GetReplacedEntryData()->first_committed_url);
  }
}

// Verify that navigating back in history does not populate the navigation
// entry's replaced entry data.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ReplacedNavigationEntryData_Back) {
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));

  // Test fixture: start with typing two URLs.
  {
    ASSERT_TRUE(NavigateToURL(shell(), url1));
    ASSERT_TRUE(NavigateToURL(shell(), url2));
    ASSERT_EQ(2, controller.GetEntryCount());
    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    NavigationEntry* entry2 = controller.GetEntryAtIndex(1);
    ASSERT_EQ(url1, entry1->GetURL());
    ASSERT_EQ(url2, entry2->GetURL());
    ASSERT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry1->GetTransitionType(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    ASSERT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry2->GetTransitionType(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_FALSE(entry1->GetReplacedEntryData().has_value());
    EXPECT_FALSE(entry2->GetReplacedEntryData().has_value());
  }

  const base::Time time1 = controller.GetEntryAtIndex(0)->GetTimestamp();

  {
    // Back.
    FrameNavigateParamsCapturer capturer(root);
    shell()->web_contents()->GetController().GoBack();
    capturer.Wait();

    // Assertions below document the current behavior.
    NavigationEntry* entry1 = controller.GetEntryAtIndex(0);
    NavigationEntry* entry2 = controller.GetEntryAtIndex(1);
    ASSERT_EQ(url1, entry1->GetURL());
    ASSERT_NE(time1, entry1->GetTimestamp());
    ASSERT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry1->GetTransitionType(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR |
                                  ui::PAGE_TRANSITION_FORWARD_BACK)));
    ASSERT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        entry2->GetTransitionType(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));

    // It is questionable whether a copy of the replaced data should be made
    // here too, because of the modified timestamp as well as the new qualifier,
    // ui::PAGE_TRANSITION_FORWARD_BACK. However, we've decided against since
    // there is no actual replacement happening.
    EXPECT_FALSE(entry1->GetReplacedEntryData().has_value());
    EXPECT_FALSE(entry2->GetReplacedEntryData().has_value());
  }
}

// Ensure we don't crash when cloning a named window.  This happened in
// https://crbug.com/603245 because neither the FrameTreeNode ID nor the name of
// the cloned window matched the root FrameNavigationEntry.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, CloneNamedWindow) {
  // Start on an initial page.
  GURL url_1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url_1));

  // Name the window.
  EXPECT_TRUE(ExecJs(shell(), "window.name = 'foo';"));

  // Navigate it.
  GURL url_2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url_2));

  // Clone the tab and load the page.
  std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone();
  WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get());
  NavigationController& new_controller = new_tab_impl->GetController();
  EXPECT_TRUE(new_controller.IsInitialNavigation());
  EXPECT_TRUE(new_controller.NeedsReload());
  {
    TestNavigationObserver clone_observer(new_tab.get());
    new_controller.LoadIfNecessary();
    clone_observer.Wait();
  }
}

// Ensure we don't crash when going back in a cloned named window.  This
// happened in https://crbug.com/603245 because neither the FrameTreeNode ID nor
// the name of the cloned window matched the root FrameNavigationEntry.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       CloneAndGoBackWithNamedWindow) {
  // Start on an initial page.
  GURL url_1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url_1));

  // Name the window.
  EXPECT_TRUE(ExecJs(shell(), "window.name = 'foo';"));

  // Navigate it.
  GURL url_2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url_2));

  // Clear the name.
  EXPECT_TRUE(ExecJs(shell(), "window.name = '';"));

  // Navigate it again.
  EXPECT_TRUE(NavigateToURL(shell(), url_1));

  // Clone the tab and load the page.
  std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone();
  WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get());
  NavigationController& new_controller = new_tab_impl->GetController();
  EXPECT_TRUE(new_controller.IsInitialNavigation());
  EXPECT_TRUE(new_controller.NeedsReload());
  {
    TestNavigationObserver clone_observer(new_tab.get());
    new_controller.LoadIfNecessary();
    clone_observer.Wait();
  }

  // Go back.
  {
    TestNavigationObserver back_load_observer(new_tab.get());
    new_controller.GoBack();
    back_load_observer.Wait();
  }
}

// Ensure that going back/forward to an apparently same document
// NavigationEntry works when the renderer process hasn't committed anything
// yet.  This can happen when using Ctrl+Back or after a crash.  See
// https://crbug.com/635403.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       BackSameDocumentInNewWindow) {
  // Start on an initial page.
  GURL url_1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url_1));

  // Perform same document navigation.
  GURL url_2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html#foo"));
  EXPECT_TRUE(NavigateToURL(shell(), url_2));

  // Clone the tab but don't load last committed page.
  std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone();
  WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get());
  NavigationController& new_controller = new_tab_impl->GetController();
  EXPECT_TRUE(new_controller.IsInitialNavigation());
  EXPECT_TRUE(new_controller.NeedsReload());

  // Go back in the new tab.
  {
    TestNavigationObserver back_load_observer(new_tab.get());
    new_controller.GoBack();
    back_load_observer.Wait();
  }

  // Make sure the new tab isn't still loading.
  EXPECT_EQ(url_1, new_controller.GetLastCommittedEntry()->GetURL());
  EXPECT_FALSE(new_tab_impl->IsLoading());

  // Also check going back in the original tab after a renderer crash.
  NavigationController& controller = shell()->web_contents()->GetController();
  RenderProcessHost* process =
      shell()->web_contents()->GetPrimaryMainFrame()->GetProcess();
  RenderProcessHostWatcher crash_observer(
      process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  process->Shutdown(0);
  crash_observer.Wait();
  {
    TestNavigationObserver back_load_observer(shell()->web_contents());
    controller.GoBack();
    back_load_observer.Wait();
  }

  // Make sure the original tab isn't still loading.
  EXPECT_EQ(url_1, controller.GetLastCommittedEntry()->GetURL());
  EXPECT_FALSE(shell()->web_contents()->IsLoading());
}

// Ensures that FrameNavigationEntries for dynamically added iframes can be
// found correctly when cloning them during a transfer.  If we don't look for
// them based on unique name in AddOrUpdateFrameEntry, the FrameTreeNode ID
// mismatch will cause us to create a second FrameNavigationEntry during the
// transfer.  Later, we'll find the wrong FrameNavigationEntry (the earlier one
// from the clone which still has a PageState), and this will cause the renderer
// to crash in NavigateInternal because the PageState is present but the page_id
// is -1 (similar to https://crbug.com/568703).  See https://crbug.com/568768.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_RepeatCreatedFrame) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // 1. Navigate the main frame.
  GURL url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_links.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  SiteInstance* main_site_instance =
      root->current_frame_host()->GetSiteInstance();

  // 2. Add a cross-site subframe.
  GURL frame_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_1.html"));
  std::string script = JsReplace(kAddFrameWithSrcScript, frame_url);
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  FrameTreeNode* subframe = root->child_at(0);
  if (AreAllSitesIsolatedForTesting()) {
    EXPECT_NE(main_site_instance,
              subframe->current_frame_host()->GetSiteInstance());
  }
  scoped_refptr<FrameNavigationEntry> subframe_entry =
      controller.GetLastCommittedEntry()->GetFrameEntry(subframe);
  EXPECT_EQ(frame_url, subframe_entry->url());

  // 3. Reload the main frame.
  {
    FrameNavigateParamsCapturer capturer(root);
    controller.Reload(ReloadType::NORMAL, false);
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_RELOAD));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());
  }

  // 4. Add the iframe again.
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }
  if (AreAllSitesIsolatedForTesting()) {
    EXPECT_NE(main_site_instance,
              root->child_at(0)->current_frame_host()->GetSiteInstance());
  }
}

// Verifies that item sequence numbers and document sequence numbers update
// properly for main frames and subframes.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_SequenceNumbers) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // 1. Navigate the main frame.
  GURL url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_links.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  scoped_refptr<FrameNavigationEntry> frame_entry =
      controller.GetLastCommittedEntry()->GetFrameEntry(root);
  int64_t isn_1 = frame_entry->item_sequence_number();
  int64_t dsn_1 = frame_entry->document_sequence_number();
  EXPECT_NE(-1, isn_1);
  EXPECT_NE(-1, dsn_1);

  // 2. Do a same document fragment navigation.
  std::string script = "document.getElementById('fraglink').click()";
  EXPECT_TRUE(ExecJs(root, script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  frame_entry = controller.GetLastCommittedEntry()->GetFrameEntry(root);
  int64_t isn_2 = frame_entry->item_sequence_number();
  int64_t dsn_2 = frame_entry->document_sequence_number();
  EXPECT_NE(-1, isn_2);
  EXPECT_NE(isn_1, isn_2);
  EXPECT_EQ(dsn_1, dsn_2);

  // 3. Add a subframe, which does an AUTO_SUBFRAME navigation.
  {
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, url)));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
  }

  // The root FrameNavigationEntry hasn't changed.
  EXPECT_EQ(frame_entry,
            controller.GetLastCommittedEntry()->GetFrameEntry(root));

  // We should have a unique ISN and DSN for the subframe entry.
  FrameTreeNode* subframe = root->child_at(0);
  scoped_refptr<FrameNavigationEntry> subframe_entry =
      controller.GetLastCommittedEntry()->GetFrameEntry(subframe);
  int64_t isn_3 = subframe_entry->item_sequence_number();
  int64_t dsn_3 = subframe_entry->document_sequence_number();
  EXPECT_NE(-1, isn_2);
  EXPECT_NE(isn_2, isn_3);
  EXPECT_NE(dsn_2, dsn_3);

  // 4. Do a same document fragment navigation in the subframe.
  EXPECT_TRUE(ExecJs(subframe, script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  subframe_entry = controller.GetLastCommittedEntry()->GetFrameEntry(subframe);
  int64_t isn_4 = subframe_entry->item_sequence_number();
  int64_t dsn_4 = subframe_entry->document_sequence_number();
  EXPECT_NE(-1, isn_4);
  EXPECT_NE(isn_3, isn_4);
  EXPECT_EQ(dsn_3, dsn_4);
}

// Verifies that the FrameNavigationEntry's redirect chain is created for the
// main frame.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_MainFrameRedirectChain) {
  NavigationControllerImpl& controller = contents()->GetController();

  // Navigate the main frame to a redirecting URL (server-side)
  GURL final_url(embedded_test_server()->GetURL("/simple_page.html"));
  GURL redirecting_url(
      embedded_test_server()->GetURL("/server-redirect?/simple_page.html"));
  NavigateToURLBlockUntilNavigationsComplete(shell(), redirecting_url, 1);
  EXPECT_TRUE(IsLastCommittedEntryOfPageType(contents(), PAGE_TYPE_NORMAL));
  EXPECT_TRUE(contents()->GetLastCommittedURL() == final_url);

  // The last committed NavigationEntry's redirect chain will contain the
  // server-side redirecting URL, then the final URL.
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntry* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
  EXPECT_EQ(entry->GetRedirectChain()[0], redirecting_url);
  EXPECT_EQ(entry->GetRedirectChain()[1], final_url);

  // No replaced entry because it's not a client-side redirect.
  EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

  // The original request URL will be the first entry of redirect chain, which
  // is the URL that initiated the server redirect.
  EXPECT_EQ(entry->GetOriginalRequestURL(), redirecting_url);

  // No referrer since this is a browser-initiated navigation.
  ExpectReferrerWithDefaultPolicy(entry, GURL());
}

// Verifies that FrameNavigationEntry's redirect chain is created and stored on
// the right subframe (AUTO_SUBFRAME navigation).
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_AutoSubFrameRedirectChain) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe_redirect.html"));
  GURL iframe_redirect_url(
      embedded_test_server()->GetURL("/server-redirect?/simple_page.html"));
  GURL iframe_final_url(embedded_test_server()->GetURL("/simple_page.html"));

  // Navigate to a page with an redirecting iframe.
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  // Check that the main frame redirect chain contains only one url.
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntryImpl* main_entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(main_entry->GetRedirectChain().size(), 1u);
  EXPECT_EQ(main_entry->GetRedirectChain()[0], main_url);

  // Check that the FrameNavigationEntry's redirect chain contains 2 urls.
  ASSERT_EQ(1U, main_entry->root_node()->children.size());
  scoped_refptr<FrameNavigationEntry> frame_entry =
      main_entry->root_node()->children[0]->frame_entry.get();
  EXPECT_EQ(frame_entry->redirect_chain().size(), 2u);
  EXPECT_EQ(frame_entry->redirect_chain()[0], iframe_redirect_url);
  EXPECT_EQ(frame_entry->redirect_chain()[1], iframe_final_url);

  // No referrer for the main frame since its last navigation is a
  // browser-initiated navigation.
  ExpectReferrerWithDefaultPolicy(main_entry, GURL());

  // The referrer for the iframe is the main frame's URL since the iframe is
  // created by the main frame at load time.
  ExpectReferrerWithDefaultPolicy(frame_entry.get(), main_url);
}

// Verifies that FrameNavigationEntry's redirect chain is created and stored on
// the right subframe (NEW_SUBFRAME navigation).
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FrameNavigationEntry_NewSubFrameRedirectChain) {
  NavigationControllerImpl& controller = contents()->GetController();
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();

  // 1. Navigate to a page with an iframe.
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_data_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  EXPECT_EQ(1, controller.GetEntryCount());

  // 2. Navigate in the subframe (browser-initiated) with a redirection.
  GURL frame_final_url(embedded_test_server()->GetURL("/simple_page.html"));
  GURL frame_redirect_url(
      embedded_test_server()->GetURL("/server-redirect?/simple_page.html"));
  NavigateFrameToURL(root->child_at(0), frame_redirect_url);

  // Check that the main frame redirect chain contains only the main_url.
  EXPECT_EQ(2, controller.GetEntryCount());
  NavigationEntryImpl* main_entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(main_entry->GetRedirectChain().size(), 1u);
  EXPECT_EQ(main_entry->GetRedirectChain()[0], main_url);

  // Check that the FrameNavigationEntry's redirect chain contains 2 urls.
  ASSERT_EQ(1U, main_entry->root_node()->children.size());
  scoped_refptr<FrameNavigationEntry> frame_entry =
      main_entry->root_node()->children[0]->frame_entry.get();
  EXPECT_EQ(frame_entry->redirect_chain().size(), 2u);
  EXPECT_EQ(frame_entry->redirect_chain()[0], frame_redirect_url);
  EXPECT_EQ(frame_entry->redirect_chain()[1], frame_final_url);

  // No referrer for the main frame since its last navigation is a
  // browser-initiated navigation.
  ExpectReferrerWithDefaultPolicy(main_entry, GURL());

  // No referrer for the iframe since its last navigation is a
  // browser-initiated navigation.
  ExpectReferrerWithDefaultPolicy(frame_entry.get(), GURL());
}

// Checks the contents of the redirect chain after same-document navigations.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_MainFrameRedirectChain_NormalThenSameDocNavigations) {
  NavigationControllerImpl& controller = contents()->GetController();

  // Navigate the main frame to a normal URL that won't cause any redirects.
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));

  {
    EXPECT_TRUE(NavigateToURL(shell(), start_url));

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    ASSERT_EQ(start_url, entry->GetURL());
    // The redirect chain contains only the URL we navigated to.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);

    // No replaced entry because it's not a client-side redirect.
    EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    // No referrer since the last navigation is a browser-initiated navigation.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }

  GURL fragment_url(embedded_test_server()->GetURL("/title1.html#foo"));
  {
    // Renderer-initiated fragment navigation with location.replace, which will
    // be treated as a client-side redirect.
    TestNavigationManager navigation_manager(contents(), fragment_url);
    EXPECT_TRUE(ExecJs(contents(), "location.replace('#foo')"));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(fragment_url, entry->GetURL());

    EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
    // On script-initiated fragment navigations, the redirect chain contains
    // the previous URL, then the fragment URL, because script-initiated
    // navigations are classified as client redirects when it does replacement.
    // So, they start with the previous page's URL in the redirect chain.
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);
    EXPECT_EQ(entry->GetRedirectChain()[1], fragment_url);

    EXPECT_TRUE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain,
    // which is the URL that initiated the client redirect.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    // No referrer (same as the referrer used for loading the document) since
    // the refererrer doesn't change on same-document navigations.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }

  {
    // History API same-document navigation through history.replaceState, which
    // will be treated as a client-side redirect.
    GURL replace_state_url(embedded_test_server()->GetURL("/title1.html#bar"));
    TestNavigationManager navigation_manager(contents(), replace_state_url);
    EXPECT_TRUE(
        ExecJs(shell(), "history.replaceState({}, '', '/title1.html#bar')"));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(replace_state_url, entry->GetURL());

    EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
    // On History API navigations with history.replaceState, the redirect chain
    // contains the previous URL, then the fragment URL, because
    // script-initiated navigations are classified as client redirects when it
    // does replacement. So, they start with the previous page's URL in the
    // redirect chain.
    EXPECT_EQ(entry->GetRedirectChain()[0], fragment_url);
    EXPECT_EQ(entry->GetRedirectChain()[1], replace_state_url);

    EXPECT_TRUE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain,
    // which is the URL that initiated the client redirect.
    // It should be client_redirect_target_url since that's what triggered
    // the replacement. But, because this is a same-document replacement,
    // it goes through the EXISTING_ENTRY path which reuses the old entry
    // and doesn't set the original request URL with the new navigation's
    // original request URL.
    // TODO(https://crbug.com/1226489): Fix this.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    // No referrer (same as the referrer used for loading the document) since
    // the refererrer doesn't change on same-document navigations.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }

  {
    // Browser-initiated fragment navigation.
    GURL fragment_url_2(embedded_test_server()->GetURL("/title1.html#baz"));
    EXPECT_TRUE(NavigateToURL(shell(), fragment_url_2));

    EXPECT_EQ(2, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(fragment_url_2, entry->GetURL());

    // On browser-initiated fragment navigations, the redirect chain contains
    // only the fragment URL, because  browser-initiated navigations aren't
    // classified as client redirects. So, they always start with an empty
    // redirect chain.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], fragment_url_2);

    EXPECT_TRUE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), fragment_url_2);

    // No referrer (same as the referrer used for loading the document) since
    // the refererrer doesn't change on same-document navigations.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }

  {
    // Renderer-initiated fragment navigation with a link click, which won't be
    // treated as a client-side redirect.
    GURL fragment_url_3(embedded_test_server()->GetURL("/title1.html#click"));
    TestNavigationManager navigation_manager(contents(), fragment_url_3);
    EXPECT_TRUE(ExecJs(contents(),
                       "let a = document.createElement('a');"
                       "a.href = 'title1.html#click';"
                       "document.body.appendChild(a);"
                       "a.click();"));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(3, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(fragment_url_3, entry->GetURL());

    // The redirect chain contains only the fragment URL, because a link click
    // isn't classified as a client redirect.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], fragment_url_3);

    EXPECT_TRUE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), fragment_url_3);

    // No referrer (same as the referrer used for loading the document) since
    // the refererrer doesn't change on same-document navigations.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }
}

// Checks the contents of the redirect chain and referrer after the main frame
// initiated navigation on its subframe.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_SubframeRedirectChain_MainFrameNavigatingSubframe) {
  NavigationControllerImpl& controller = contents()->GetController();

  // Navigate the main frame to a page with an iframe.
  GURL start_url(embedded_test_server()->GetURL("/page_with_iframe.html"));
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  FrameTreeNode* iframe = nullptr;
  GURL iframe_url;

  {
    EXPECT_TRUE(NavigateToURL(shell(), start_url));

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    ASSERT_EQ(start_url, entry->GetURL());
    // The redirect chain contains only the URL we navigated to.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);

    // No replaced entry because it's not a client-side redirect.
    EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    iframe = root->child_at(0);
    iframe_url = iframe->current_url();

    scoped_refptr<FrameNavigationEntry> frame_entry =
        controller.GetLastCommittedEntry()->GetFrameEntry(iframe);
    EXPECT_EQ(frame_entry->redirect_chain().size(), 1u);
    EXPECT_EQ(frame_entry->redirect_chain()[0], iframe_url);

    // The referrer for the iframe is the main frame's URL since the iframe is
    // created by the main frame at load time.
    ExpectReferrerWithDefaultPolicy(frame_entry.get(), start_url);
  }

  GURL fragment_url(embedded_test_server()->GetURL("/title1.html#foo"));
  {
    // Renderer-initiated fragment navigation on the iframe, triggered by the
    // main frame setting the src attribute.
    TestNavigationManager navigation_manager(contents(), fragment_url);
    EXPECT_TRUE(ExecJs(
        contents(), JsReplace("document.getElementById('test_iframe').src = $1",
                              fragment_url)));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(2, controller.GetEntryCount());
    scoped_refptr<FrameNavigationEntry> frame_entry =
        controller.GetLastCommittedEntry()->GetFrameEntry(iframe);
    EXPECT_EQ(fragment_url, frame_entry->url());

    EXPECT_EQ(frame_entry->redirect_chain().size(), 1u);
    // Script-initiated fragment navigations are considered as client redirect
    // only when only when it does replacement.
    // This script-initiated fragment navigations is not a client redirect
    // since it doesn't do replacement. So it only contains the
    // fragment URL in its redirect chain.
    EXPECT_EQ(frame_entry->redirect_chain()[0], fragment_url);

    // The referrer is the main frame's URL (same as the referrer used for
    // loading the document) since the refererrer doesn't change on
    // same-document navigations.
    ExpectReferrerWithDefaultPolicy(frame_entry.get(), start_url);
  }

  GURL fragment_url_2(embedded_test_server()->GetURL("/title1.html#bar"));
  {
    // Renderer-initiated fragment navigation on the iframe, triggered by a link
    // click in the main frame.
    TestNavigationManager navigation_manager(contents(), fragment_url_2);
    EXPECT_TRUE(ExecJs(contents(),
                       "let a = document.createElement('a');"
                       "a.href = 'title1.html#bar';"
                       "a.target = 'test_iframe';"
                       "document.body.appendChild(a);"
                       "a.click();"));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(3, controller.GetEntryCount());
    scoped_refptr<FrameNavigationEntry> frame_entry =
        controller.GetLastCommittedEntry()->GetFrameEntry(iframe);
    EXPECT_EQ(fragment_url_2, frame_entry->url());

    // The redirect chain only contains the final fragment URL because we don't
    // consider the navigation as a client side redirect.
    EXPECT_EQ(frame_entry->redirect_chain().size(), 1u);
    EXPECT_EQ(frame_entry->redirect_chain()[0], fragment_url_2);

    // The referrer is the main frame's URL (same as the referrer used for
    // loading the document) since the refererrer doesn't change on
    // same-document navigations.
    ExpectReferrerWithDefaultPolicy(frame_entry.get(), start_url);
  }

  GURL cross_document_url(embedded_test_server()->GetURL("/title2.html"));
  {
    // Renderer-initiated cross-document navigation on the iframe, triggered by
    // a link click in the main frame.
    TestNavigationManager navigation_manager(contents(), cross_document_url);
    EXPECT_TRUE(ExecJs(contents(),
                       "let a = document.createElement('a');"
                       "a.href = 'title2.html';"
                       "a.target = 'test_iframe';"
                       "document.body.appendChild(a);"
                       "a.click();"));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(4, controller.GetEntryCount());
    scoped_refptr<FrameNavigationEntry> frame_entry =
        controller.GetLastCommittedEntry()->GetFrameEntry(iframe);
    EXPECT_EQ(cross_document_url, frame_entry->url());

    // The redirect chain only contains the final URL because we don't consider
    // the navigation as a client side redirect.
    EXPECT_EQ(frame_entry->redirect_chain().size(), 1u);
    EXPECT_EQ(frame_entry->redirect_chain()[0], cross_document_url);

    // The referrer is the main frame's URL since the cross-document navigation
    // is started by a link click in the main frame.
    ExpectReferrerWithDefaultPolicy(frame_entry.get(), start_url);
  }
}

// Checks the contents of the redirect chain after same-site navigations.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_MainFrameRedirectChain_NormalThenSameSiteNavigations) {
  NavigationControllerImpl& controller = contents()->GetController();

  // Navigate the main frame to a normal URL that won't cause any redirects.
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));

  {
    EXPECT_TRUE(NavigateToURL(shell(), start_url));

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    ASSERT_EQ(start_url, entry->GetURL());
    // The redirect chain contains only the URL we navigated to.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);

    // No replaced entry because it's not a client-side redirect.
    EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    // No referrer since the last navigation is a browser-initiated navigation.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }

  GURL url_2(embedded_test_server()->GetURL("/title2.html"));
  {
    FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                              ->GetPrimaryFrameTree()
                              .root();

    // Renderer-initiated same-site navigation with location.replace,
    // causing it to be treated as a client redirect.
    TestNavigationObserver navigation_observer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, JsReplace("location.replace($1)", url_2)));
    navigation_observer.WaitForNavigationFinished();

    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(url_2, entry->GetURL());

    // On script-initiated same-site navigations with no server redirects, the
    // redirect chain contains the previous page's URL, because script-initiated
    // navigations are classified as client redirects when it does replacement.
    // So, they start with the previous page's URL in the redirect chain.
    EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);
    EXPECT_EQ(entry->GetRedirectChain()[1], url_2);

    EXPECT_TRUE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain,
    // which is the URL that initiated the client redirect.
    EXPECT_EQ(entry->GetOriginalRequestURL(), entry->GetRedirectChain()[0]);

    // The referrer is |start_url| (the previous URL) since the same-site
    // navigation is considered a client-side redirect.
    ExpectReferrerWithDefaultPolicy(entry, start_url);
  }

  {
    // Browser-initiated same-site navigation.
    GURL url_3(embedded_test_server()->GetURL("/title3.html"));
    EXPECT_TRUE(NavigateToURL(shell(), url_3));

    EXPECT_EQ(2, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(url_3, entry->GetURL());

    // On browser-initiated same-site navigations with no redirects, the
    // redirect chain contains only the navigating URL, because
    // browser-initiated navigations aren't classified as client redirects. So,
    // they always start with an empty redirect chain.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], url_3);

    // No replaced entry because it's not a client-side redirect.
    EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), url_3);

    // No referrer since the last navigation is a browser-initiated navigation.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }
}

// Checks the contents of the redirect chain after cross-site navigations.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_MainFrameRedirectChain_NormalThenCrossSiteNavigations) {
  NavigationControllerImpl& controller = contents()->GetController();

  // Navigate the main frame to a normal URL that won't cause any redirects.
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));

  {
    EXPECT_TRUE(NavigateToURL(shell(), start_url));

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    ASSERT_EQ(start_url, entry->GetURL());
    // The redirect chain contains only the URL we navigated to.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);

    // No replaced entry because it's not a client-side redirect.
    EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    // No referrer since the last navigation is a browser-initiated navigation.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }

  GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
  {
    // Do a renderer-initiated cross-site navigation using location.replace
    // that's considered a client-side redirect.
    TestNavigationObserver nav_observer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(shell(), JsReplace("location.replace($1)", url_2)));
    nav_observer.Wait();

    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(url_2, entry->GetURL());

    // On script-initiated cross-site navigations with no server redirects, the
    // redirect chain contains the previous page's URL, because script-initiated
    // navigations are classified as client redirects when they do replacement.
    // So, they start with the previous page's URL in the redirect chain.
    EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);
    EXPECT_EQ(entry->GetRedirectChain()[1], url_2);

    EXPECT_TRUE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain,
    // which is the URL that initiated the client redirect.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    // The referrer is |start_url| since the cross-site navigation is
    // renderer-initiated (and considered a client-side redirect)
    // TODO(https://crbug.com/1218786): This uses the full URL instead of only
    // the origin even though it went cross-origin, because of the special
    // handling of client-side redirects in the referrer calculation for the
    // FrameNavigationEntry. This might not be the same as the referrer value
    // used by the navigation request (which is correctly sanitized to only
    // contain the origin). Maybe fix this?
    ExpectReferrerWithDefaultPolicy(entry, start_url);
  }

  GURL url_3(embedded_test_server()->GetURL("c.com", "/title3.html"));
  {
    // Do a renderer-initiated cross-site navigation that's not considered a
    // client-side redirect.
    TestNavigationManager navigation_manager(contents(), url_3);
    EXPECT_TRUE(
        ExecJs(contents(), JsReplace("let a = document.createElement('a');"
                                     "a.href = $1;"
                                     "document.body.appendChild(a);"
                                     "a.click();",
                                     url_3)));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(2, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(url_3, entry->GetURL());

    // Since the navigation is not a client-side redirect and did not encounter
    // server-side redirects, the redirect chain only contains one URL: |url_3|.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], url_3);

    // The original request URL will be the first entry of redirect chain,
    // which is the URL that initiated the client redirect.
    EXPECT_EQ(entry->GetOriginalRequestURL(), url_3);

    // The referrer is |url_2| since it's the URL that started the navigation.
    // Since the navigation went through a cross-origin redirect and the default
    // referrer policy strips referrers to their origins on cross-origin
    // requests, only the origin is saved.
    ExpectReferrerWithDefaultPolicy(entry, url_2.DeprecatedGetOriginAsURL());
  }

  {
    // Browser-initiated cross-site navigation.
    GURL url_4(embedded_test_server()->GetURL("d.com", "/empty.html"));
    EXPECT_TRUE(NavigateToURL(shell(), url_4));

    EXPECT_EQ(3, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(url_4, entry->GetURL());

    // On browser-initiated cross-site navigations with no redirects, the
    // redirect chain contains only the navigating URL, because
    // browser-initiated navigations aren't classified as client redirects. So,
    // they always start with an empty redirect chain.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], url_4);

    // No replaced entry because it's not a client-side redirect.
    EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), url_4);

    // No referrer since the last navigation is a browser-initiated navigation.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }
}

// Verifies that the FrameNavigationEntry's redirect chain and referrer is
// correct for the main frame after a cross-origin navigation triggered by
// setting location.replace, which is considered a client redirect.
// location.replace is used instead of location.href as frame navigation is
// considered to be a client redirect only if it does replacement.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_MainFrameRedirectChain_CrossOriginClientRedirect_LocationReplace) {
  NavigationControllerImpl& controller = contents()->GetController();

  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  EXPECT_EQ(1, controller.GetEntryCount());

  // Navigate the main frame to a redirecting URL (server-side) by setting
  // location.replace, which should count as a client side redirect.

  GURL target_url(embedded_test_server()->GetURL("b.com", "/simple_page.html"));
  TestNavigationManager navigation_manager(contents(), target_url);
  EXPECT_TRUE(
      ExecJs(contents(), JsReplace("location.replace($1);", target_url)));
  ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

  // The last committed NavigationEntry's redirect chain will contain the
  // client-side redirector URL, then the target URL.
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntry* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
  EXPECT_EQ(entry->GetRedirectChain()[0], start_url);
  EXPECT_EQ(entry->GetRedirectChain()[1], target_url);

  // The original request URL will be the first entry of redirect chain, which
  // is the URL that initiated the server redirect.
  EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

  // The referrer is |start_url| since the navigation is considered a
  // client-side redirect so it uses the previous document's URL.
  // TODO(https://crbug.com/1218786): This uses the full URL instead of only
  // the origin even though it went cross-origin, because of the special
  // handling of client-side redirects in the referrer calculation for the
  // FrameNavigationEntry. This might not be the same as the referrer value
  // used by the navigation request (which is correctly sanitized to only
  // contain the origin). Maybe fix this?
  ExpectReferrerWithDefaultPolicy(entry, start_url);
}

// Verifies that the FrameNavigationEntry's redirect chain and referrer is
// correct for the main frame after an initially same-origin navigation that
// ended up as cross origin, triggered by setting location.replace.
// location.replace is used instead of location.href as frame navigation is
// considered to be a client redirect only if it does replacement.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_MainFrameRedirectChain_SameOriginThenCrossOriginRedirect_LocationReplace) {
  NavigationControllerImpl& controller = contents()->GetController();

  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  EXPECT_EQ(1, controller.GetEntryCount());

  // Navigate the main frame to a redirecting URL (server-side) by setting
  // location.replace, which should count as a client side redirect.
  GURL final_url(embedded_test_server()->GetURL("b.com", "/simple_page.html"));
  GURL redirecting_url(
      embedded_test_server()->GetURL("/server-redirect?" + final_url.spec()));
  TestNavigationManager navigation_manager(contents(), redirecting_url);
  EXPECT_TRUE(
      ExecJs(contents(), JsReplace("location.replace($1);", redirecting_url)));
  ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

  // The last committed NavigationEntry's redirect chain will contain the
  // client-side redirector URL, server-side redirecting URL, then the final
  // URL.
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntry* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(entry->GetRedirectChain().size(), 3u);
  EXPECT_EQ(entry->GetRedirectChain()[0], start_url);
  EXPECT_EQ(entry->GetRedirectChain()[1], redirecting_url);
  EXPECT_EQ(entry->GetRedirectChain()[2], final_url);

  // The original request URL will be the first entry of redirect chain, which
  // is the URL that initiated the server redirect.
  EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

  // The referrer is |start_url| since the navigation is considered a
  // client-side redirect so it uses the previous document's URL.
  // TODO(https://crbug.com/1218786): This uses the full URL instead of only
  // the origin even though it went cross-origin, because of the special
  // handling of client-side redirects in the referrer calculation for the
  // FrameNavigationEntry. This might not be the same as the referrer value
  // used by the navigation request (which is correctly sanitized to only
  // contain the origin). Maybe fix this?
  ExpectReferrerWithDefaultPolicy(entry, start_url);
}

// Verifies that the FrameNavigationEntry's redirect chain and referrer is
// correct for the main frame after an initially same-origin navigation that
// ended up as cross origin, triggered by a link click.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_MainFrameRedirectChain_SameOriginThenCrossOriginRedirect_LinkClick) {
  NavigationControllerImpl& controller = contents()->GetController();

  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  EXPECT_EQ(1, controller.GetEntryCount());

  // Navigate the main frame to a redirecting URL (server-side) by clicking a
  // link, which should not count as a client side redirect.
  GURL final_url(embedded_test_server()->GetURL("b.com", "/simple_page.html"));
  GURL redirecting_url(
      embedded_test_server()->GetURL("/server-redirect?" + final_url.spec()));

  TestNavigationManager navigation_manager(contents(), redirecting_url);
  EXPECT_TRUE(
      ExecJs(contents(), JsReplace("let a = document.createElement('a');"
                                   "a.href = $1;"
                                   "document.body.appendChild(a);"
                                   "a.click();",
                                   redirecting_url)));
  ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

  // The last committed NavigationEntry's redirect chain will contain the
  // server-side redirecting URL and the final URL.
  EXPECT_EQ(2, controller.GetEntryCount());
  NavigationEntry* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
  EXPECT_EQ(entry->GetRedirectChain()[0], redirecting_url);
  EXPECT_EQ(entry->GetRedirectChain()[1], final_url);

  // The original request URL will be the first entry of redirect chain, which
  // is the URL that initiated the server redirect.
  EXPECT_EQ(entry->GetOriginalRequestURL(), redirecting_url);

  // The referrer is |start_url| since it's the URL that started the navigation.
  // Since the navigation went through a cross-origin redirect and the default
  // referrer policy strips referrers to their origins on cross-origin
  // requests, only the origin is saved.
  ExpectReferrerWithDefaultPolicy(entry, start_url.DeprecatedGetOriginAsURL());
}

// Verifies that the FrameNavigationEntry's redirect chain and referrer is
// correct for the main frame after a navigation triggered by a link click to a
// URL that started out as cross origin but ended up being same origin to the
// original page.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_MainFrameRedirectChain_CrossOriginThenSameOriginRedirect_LinkClick) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  EXPECT_EQ(1, controller.GetEntryCount());

  // Navigate the main frame to a redirecting URL (server-side) by clicking a
  // link, which should not count as a client side redirect. The link started
  // out cross-origin but ended up being in the same origin as the current
  // document.
  GURL final_url(embedded_test_server()->GetURL("/simple_page.html"));
  GURL redirecting_url(embedded_test_server()->GetURL(
      "b.com", "/server-redirect?" + final_url.spec()));

  TestNavigationManager navigation_manager(shell()->web_contents(),
                                           redirecting_url);
  EXPECT_TRUE(
      ExecJs(contents(), JsReplace("let a = document.createElement('a');"
                                   "a.href = $1;"
                                   "document.body.appendChild(a);"
                                   "a.click();",
                                   redirecting_url)));
  ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

  // The last committed NavigationEntry's redirect chain will contain the
  // server-side redirecting URL and the final URL.
  EXPECT_EQ(2, controller.GetEntryCount());
  content::NavigationEntry* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
  EXPECT_EQ(entry->GetRedirectChain()[0], redirecting_url);
  EXPECT_EQ(entry->GetRedirectChain()[1], final_url);

  // The original request URL will be the first entry of redirect chain, which
  // is the URL that initiated the server redirect.
  EXPECT_EQ(entry->GetOriginalRequestURL(), redirecting_url);

  // The referrer is |start_url| since it's the URL that started the navigation.
  // Since the navigation went through a cross-origin URL (even though it ended
  // up on a same-origin URL after redirects), only the origin is saved.
  ExpectReferrerWithDefaultPolicy(entry, start_url.DeprecatedGetOriginAsURL());
}

// Checks the contents of the redirect chain after reloads.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_MainFrameRedirectChain_NormalThenReloads) {
  NavigationControllerImpl& controller = contents()->GetController();

  // Navigate the main frame to a normal URL that won't cause any redirects.
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));

  {
    EXPECT_TRUE(NavigateToURL(shell(), start_url));

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    ASSERT_EQ(start_url, entry->GetURL());
    // The redirect chain contains only the URL we navigated to.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);

    // No replaced entry because it's not a client-side redirect.
    EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    // No referrer since the last navigation is a browser-initiated navigation.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }

  {
    // Browser-initiated tab reload.
    TestNavigationManager navigation_manager(contents(), start_url);
    shell()->Reload();
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(start_url, entry->GetURL());

    // On browser-initiated tab reloads with no redirects, the redirect
    // chain only contains the original URL once, because browser-initiated
    // navigations aren't classified as client redirects. So, they always start
    // with an empty redirect chain.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);

    // There is no "replaced entry".
    EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    // A browser-initiated reload reuses the most recent entry's referrer; since
    // the most recent navigation before the reload was browser-initiated, the
    // referrer URL is empty.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }

  {
    // Renderer-initiated reload.
    TestNavigationManager navigation_manager(contents(), start_url);
    EXPECT_TRUE(ExecJs(contents(), "location.reload();"));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(start_url, entry->GetURL());

    // On main frame script-initiated reloads with no redirects, the redirect
    // chain contains the reloaded page's URL twice, because script-initiated
    // navigations are always classified as client redirects. So, they start
    // with the reloaded page's URL in the redirect chain.
    EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);
    EXPECT_EQ(entry->GetRedirectChain()[1], start_url);

    // The URL is saved as the "replaced entry" because it's a reload.
    ASSERT_TRUE(entry->GetReplacedEntryData().has_value());
    EXPECT_EQ(start_url, entry->GetReplacedEntryData()->first_committed_url);

    // The original request URL will be the first entry of redirect chain,
    // which is the URL that initiated the client redirect.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    // The referrer is |start_url| since this is a renderer-initiated reload.
    ExpectReferrerWithDefaultPolicy(entry, start_url);
  }

  {
    // Browser-initiated tab reload after a renderer-initiated reload.
    TestNavigationManager navigation_manager(contents(), start_url);
    shell()->Reload();
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(start_url, entry->GetURL());

    // On browser-initiated tab reloads with no redirects, the redirect
    // chain only contains the original URL once, because browser-initiated
    // navigations aren't classified as client redirects. So, they always start
    // with an empty redirect chain.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);

    // The URL is saved as the "replaced entry" because we reuse the entry's
    // replaced entry on reloads.
    ASSERT_TRUE(entry->GetReplacedEntryData().has_value());
    EXPECT_EQ(start_url, entry->GetReplacedEntryData()->first_committed_url);

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    // The referrer is |start_url| since we reuse the entry's referrer in
    // browser-initiated reloads.
    ExpectReferrerWithDefaultPolicy(entry, start_url);
  }

  {
    // Browser-initiated frame reload. Note that this is different than tab
    // reload, as this case goes through NavigationControllerImpl::ReloadFrame()
    // instead of NavigationControllerImpl::Reload().
    TestNavigationManager navigation_manager(contents(), start_url);
    contents()->GetPrimaryMainFrame()->Reload();
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(start_url, entry->GetURL());

    // On main-frame browser-initiated frame reloads with no redirects, the
    // redirect chain only contains the original URL once.
    // This should've contained two copies of `start_url`, because the browser
    // actually sent the previous FNE's redirect chain at commit, containing the
    // one entry of `start_url` from before. The renderer should've added one
    // more entry of `start_url` (as the current document URL) and sent that
    // back to the browser, but the renderer actually thought that the redirect
    // chain is empty (because it checked for the redirect_response array,
    // instead of the redirects array). So we end up with a redirect chain of
    // size 1.
    // TODO(https://crbug.com/1171225): Fix this.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);

    // The URL is saved as the "replaced entry" because we reuse the entry's
    // replaced entry on reloads.
    ASSERT_TRUE(entry->GetReplacedEntryData().has_value());
    EXPECT_EQ(start_url, entry->GetReplacedEntryData()->first_committed_url);

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    // The referrer is |start_url| since we reuse the entry's referrer in
    // browser-initiated reloads.
    ExpectReferrerWithDefaultPolicy(entry, start_url);
  }
}

// Checks the contents of the redirect chain after reloads on a subframe.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_MainFrameRedirectChain_NormalThenReloads_Subframe) {
  NavigationControllerImpl& controller = contents()->GetController();

  // Navigate the main frame to a normal URL that won't cause any redirects and
  // has an iframe.
  GURL start_url(embedded_test_server()->GetURL("/page_with_iframe.html"));

  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  FrameTreeNode* iframe = nullptr;
  GURL iframe_url;

  {
    EXPECT_TRUE(NavigateToURL(shell(), start_url));

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    ASSERT_EQ(start_url, entry->GetURL());
    // The redirect chain contains only the URL we navigated to.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);

    // No replaced entry because it's not a client-side redirect.
    EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    iframe = root->child_at(0);
    iframe_url = iframe->current_url();

    scoped_refptr<FrameNavigationEntry> frame_entry =
        controller.GetLastCommittedEntry()->GetFrameEntry(iframe);
    EXPECT_EQ(frame_entry->redirect_chain().size(), 1u);
    EXPECT_EQ(frame_entry->redirect_chain()[0], iframe_url);

    // The referrer for the iframe is the main frame's URL since the iframe is
    // created by the main frame at load time.
    ExpectReferrerWithDefaultPolicy(frame_entry.get(), start_url);
  }

  {
    // Browser-initiated reload.
    TestNavigationManager navigation_manager(contents(), iframe_url);
    root->child_at(0)->current_frame_host()->Reload();
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(1, controller.GetEntryCount());
    scoped_refptr<FrameNavigationEntry> frame_entry =
        controller.GetLastCommittedEntry()->GetFrameEntry(iframe);
    EXPECT_EQ(iframe_url, frame_entry->url());

    // On subframe browser-initiated reloads, the redirect chain contains two
    // copies of `iframe_url`, because we reused the previous FNE's redirect
    // chain at commit, containing the two entries seen above, and we added one
    // more entry of `iframe_url` (as the current document URL).
    EXPECT_EQ(frame_entry->redirect_chain().size(), 2u);
    EXPECT_EQ(frame_entry->redirect_chain()[0], iframe_url);
    EXPECT_EQ(frame_entry->redirect_chain()[1], iframe_url);

    // The referrer is |start_url| since we reuse the entry's referrer in
    // browser-initiated reloads.
    ExpectReferrerWithDefaultPolicy(frame_entry.get(), start_url);
  }

  {
    // Renderer-initiated reload on the iframe.
    TestNavigationManager navigation_manager(contents(), iframe_url);
    EXPECT_TRUE(ExecJs(iframe, "location.reload();"));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(1, controller.GetEntryCount());
    scoped_refptr<FrameNavigationEntry> frame_entry =
        controller.GetLastCommittedEntry()->GetFrameEntry(iframe);
    EXPECT_EQ(iframe_url, frame_entry->url());

    // On renderer-initiated reloads with no redirects, the redirect chain
    // contains the reloaded page's URL twice.
    EXPECT_EQ(frame_entry->redirect_chain().size(), 2u);
    EXPECT_EQ(frame_entry->redirect_chain()[0], iframe_url);
    EXPECT_EQ(frame_entry->redirect_chain()[1], iframe_url);

    // The referrer is |iframe_url| since this is a renderer-initiated reload.
    ExpectReferrerWithDefaultPolicy(frame_entry.get(), iframe_url);
  }

  {
    // Browser-initiated reload after a renderer-initiated reload.
    TestNavigationManager navigation_manager(contents(), iframe_url);
    root->child_at(0)->current_frame_host()->Reload();
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(1, controller.GetEntryCount());
    scoped_refptr<FrameNavigationEntry> frame_entry =
        controller.GetLastCommittedEntry()->GetFrameEntry(iframe);
    EXPECT_EQ(iframe_url, frame_entry->url());

    // On subframe browser-initiated reloads, the redirect chain contains three
    // copies of `iframe_url`, because we reused the previous FNE's redirect
    // chain at commit, containing the two entries seen above, and we added one
    // more entry of `iframe_url` (as the current document URL).
    EXPECT_EQ(frame_entry->redirect_chain().size(), 3u);
    EXPECT_EQ(frame_entry->redirect_chain()[0], iframe_url);
    EXPECT_EQ(frame_entry->redirect_chain()[1], iframe_url);
    EXPECT_EQ(frame_entry->redirect_chain()[2], iframe_url);

    // The referrer is the same as the previous navigation since this is a
    // browser-initiated reload and we reuse the entry's referrer.
    ExpectReferrerWithDefaultPolicy(frame_entry.get(), iframe_url);
  }
}

// Checks the contents of the redirect chain after navigation to an error page.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_MainFrameRedirectChain_NormalThenErrorPage) {
  NavigationControllerImpl& controller = contents()->GetController();

  // Navigate the main frame to a normal URL that won't cause any redirects.
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));

  {
    EXPECT_TRUE(NavigateToURL(shell(), start_url));

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    ASSERT_EQ(start_url, entry->GetURL());
    // The redirect chain contains only the URL we navigated to.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);

    // No replaced entry because it's not a client-side redirect.
    EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    // No referrer since the last navigation is a browser-initiated navigation.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }

  GURL url_2(embedded_test_server()->GetURL("b.com", "/empty404.html"));
  {
    // Renderer-initiated cross-site navigation to an error page.
    EXPECT_FALSE(NavigateToURLFromRenderer(shell(), url_2));

    EXPECT_EQ(2, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(url_2, entry->GetURL());

    // On renderer-initiated cross-site navigations with no redirects that end
    // up in error pages, the redirect chain contains the final URL.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], url_2);

    // No replaced entry because it's not a client-side redirect.
    EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

    // The original request URL on navigations that end up in an error page will
    // be the URL of the page that failed to load.
    EXPECT_EQ(entry->GetOriginalRequestURL(), url_2);

    // The referrer is the previous page's URL but only the origin since it went
    // cross-origin, and even though it is using a renderer-initiated method
    // that is typically classified as a client side redirect, it is not
    // considered as a client-side redirect since we end up in an error page.
    // (Non-error client-side redirects will always return the full URL).
    ExpectReferrerWithDefaultPolicy(entry,
                                    start_url.DeprecatedGetOriginAsURL());
  }

  {
    // Browser-initiated cross-site navigation to an error page.
    GURL url_3(embedded_test_server()->GetURL("c.com", "/empty404.html"));
    EXPECT_FALSE(NavigateToURL(shell(), url_3));

    EXPECT_EQ(3, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(url_3, entry->GetURL());

    // On browser-initiated cross-site navigations with no redirects that end
    // up in error pages, the redirect chain contains the final URL.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], url_3);

    // No replaced entry because it's not a client-side redirect.
    EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

    // The original request URL on navigations that end up in an error page will
    // be the URL of the page that failed to load.
    EXPECT_EQ(entry->GetOriginalRequestURL(), url_3);

    // No referrer since the last navigation is a browser-initiated navigation.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }
}

// Checks the contents of the redirect chain after a browser-initiated
// navigation that server-redirects to an error page.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_ServerRedirectToErrorPage_BrowserInitiated) {
  NavigationControllerImpl& controller = contents()->GetController();

  GURL server_redirecting_url(
      embedded_test_server()->GetURL("/server-redirect?/empty404.html"));

  // Browser-initiated same-site navigation that server-redirects to an empty
  // 404 page, which would result in an error page.
  EXPECT_FALSE(NavigateToURL(shell(), server_redirecting_url));

  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntry* entry = controller.GetLastCommittedEntry();
  GURL fail_url(embedded_test_server()->GetURL("/empty404.html"));
  EXPECT_EQ(fail_url, entry->GetURL());

  // On navigations that end up in error pages, the redirect chain only
  // contains the final URL, even if the navigation went through server
  // redirects.
  EXPECT_THAT(entry->GetRedirectChain(),
              testing::ElementsAre(server_redirecting_url, fail_url));

  // No replaced entry because it's not a client-side redirect.
  EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

  // The original request URL on navigations that end up in an error page will
  // still be the iniital URL.
  EXPECT_EQ(entry->GetOriginalRequestURL(), server_redirecting_url);

  // No referrer since the last navigation is a browser-initiated navigation.
  ExpectReferrerWithDefaultPolicy(entry, GURL());
}

// Checks the contents of the redirect chain after a renderer-initiated
// navigation that server-redirects to an error page.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_ServerRedirectToErrorPage_RendererInitiated) {
  NavigationControllerImpl& controller = contents()->GetController();

  // Navigate the main frame to a normal URL that won't cause any redirects, so
  // that we can do a renderer-initiated navigation after this.
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));

  {
    EXPECT_TRUE(NavigateToURL(shell(), start_url));

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    ASSERT_EQ(start_url, entry->GetURL());
    // The redirect chain contains only the URL we navigated to.
    EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
    EXPECT_EQ(entry->GetRedirectChain()[0], start_url);

    // No replaced entry because it's not a client-side redirect.
    EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

    // The original request URL will be the first entry of redirect chain, which
    // is also the final URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);

    // No referrer since the last navigation is a browser-initiated navigation.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }

  {
    // Renderer-initiated same-site navigation that server-redirects to an
    // empty 404 page, which would result in an error page. Note that since this
    // is a script-initiated navigation, it will be marked as a client redirect
    // too.
    GURL server_redirecting_url(
        embedded_test_server()->GetURL("/server-redirect?/empty404.html"));
    GURL fail_url(embedded_test_server()->GetURL("/empty404.html"));
    EXPECT_FALSE(NavigateToURLFromRenderer(shell(), server_redirecting_url));

    EXPECT_EQ(2, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(fail_url, entry->GetURL());

    // On navigations that end up in error pages, the redirect chain should
    // still contain the original and all the intermediate URLs.
    EXPECT_THAT(entry->GetRedirectChain(),
                testing::ElementsAre(server_redirecting_url, fail_url));

    // No replaced entry because it's not a client-side redirect.
    EXPECT_FALSE(entry->GetReplacedEntryData().has_value());

    // The original request URL on navigations that end up in an error page will
    // still be the iniital URL.
    EXPECT_EQ(entry->GetOriginalRequestURL(), server_redirecting_url);

    // The referrer is the previous page's URL since this is renderer-initiated,
    // but is not considered as a client-side redirect since we end up in an
    // error page.
    ExpectReferrerWithDefaultPolicy(entry, start_url);
  }
}

// Tests navigating from an error page that server-redirects to the current URL
// (which results in an error page again).
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ServerRedirectToSameURLErrorPage) {
  NavigationControllerImpl& controller = contents()->GetController();

  GURL fail_url(embedded_test_server()->GetURL("/empty404.html"));
  GURL server_redirecting_url(
      embedded_test_server()->GetURL("/server-redirect?/empty404.html"));

  // Navigate to a URL that will result in an error page.
  EXPECT_FALSE(NavigateToURL(shell(), fail_url));
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntry* last_entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(fail_url, last_entry->GetURL());

  // Navigate to a URL that redirects to the same URL we're currently on, which
  // will commit an error page again.
  EXPECT_FALSE(NavigateToURL(shell(), server_redirecting_url, fail_url));

  // The navigation will do a replacement.
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_NE(last_entry, controller.GetLastCommittedEntry());
  last_entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(fail_url, last_entry->GetURL());

  // We replaced the previous entry.
  EXPECT_TRUE(last_entry->GetReplacedEntryData().has_value());
  EXPECT_EQ(fail_url, last_entry->GetReplacedEntryData()->first_committed_url);

  // No referrer since the last navigation is a browser-initiated navigation.
  ExpectReferrerWithDefaultPolicy(last_entry, GURL());
}

// Checks the contents of the redirect chain after client-side redirect to a
// different document than the original URL.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_MainFrameRedirectChain_ClientRedirectThenFragment) {
  NavigationControllerImpl& controller = contents()->GetController();

  // Navigate the main frame to a redirecting URL (client-side) that will
  // redirect to a different document than the original URL's document.
  GURL client_redirecting_url(embedded_test_server()->GetURL(
      "/navigation_controller/client_redirect.html"));
  GURL client_redirect_target_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));

  {
    // Client-side redirects will result in a new navigation, so wait for two
    // navigations to finish.
    TestNavigationManager navigation_manager_1(contents(),
                                               client_redirecting_url);
    TestNavigationManager navigation_manager_2(contents(),
                                               client_redirect_target_url);
    shell()->LoadURL(client_redirecting_url);
    ASSERT_TRUE(navigation_manager_1
                    .WaitForNavigationFinished());  // Initial navigation.
    ASSERT_TRUE(navigation_manager_2
                    .WaitForNavigationFinished());  // Client-side redirect.

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetEntryAtIndex(0);
    ASSERT_EQ(client_redirect_target_url, entry->GetURL());
    EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
    // When a client-side redirect happens after a navigation, the redirect
    // chain contains the URL that triggers the client redirect first, then the
    // URL that we got redirected to.
    EXPECT_EQ(entry->GetRedirectChain()[0], client_redirecting_url);
    EXPECT_EQ(entry->GetRedirectChain()[1], client_redirect_target_url);

    // The client-redirecting URL is saved as the "replaced entry".
    ASSERT_TRUE(entry->GetReplacedEntryData().has_value());
    EXPECT_EQ(client_redirecting_url,
              entry->GetReplacedEntryData()->first_committed_url);

    // The original request URL will be the first entry of redirect chain,
    // which is the URL that initiated the client redirect.
    EXPECT_EQ(entry->GetOriginalRequestURL(), client_redirecting_url);

    // The referrer is |client_redirecting_url| since the navigation is a
    // client-side redirect.
    ExpectReferrerWithDefaultPolicy(entry, client_redirecting_url);
  }

  {
    // Renderer-initiated fragment navigation that does replacement.
    GURL fragment_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_1.html#foo"));
    TestNavigationManager navigation_manager(contents(), fragment_url);
    EXPECT_TRUE(ExecJs(contents(), "location.replace('#foo');"));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(fragment_url, entry->GetURL());

    EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
    // When a renderer-initiated fragment navigation that does replacement
    // happens after a client-side redirect navigation, the redirect chain
    // will contain the previous URL
    // (the URL we ended up in after client redirect), then the fragment URL.
    EXPECT_EQ(entry->GetRedirectChain()[0], client_redirect_target_url);
    EXPECT_EQ(entry->GetRedirectChain()[1], fragment_url);

    // The client-redirecting URL is still saved as the "replaced entry".
    EXPECT_TRUE(entry->GetReplacedEntryData().has_value());
    EXPECT_EQ(client_redirecting_url,
              entry->GetReplacedEntryData()->first_committed_url);

    // The original request URL will be the first entry of redirect chain,
    // which is the URL that initiated the client redirect.
    // It should be client_redirect_target_url since that's what triggered
    // the replacement. But, because this is a same-document replacement,
    // it goes through the EXISTING_ENTRY path which reuses the old entry
    // and doesn't set the original request URL with the new navigation's
    // original request URL.
    // TODO(https://crbug.com/1226489): Fix this.
    EXPECT_EQ(entry->GetOriginalRequestURL(), client_redirecting_url);

    // The referrer is |client_redirecting_url| (same as the referrer used for
    // loading the document) since the referrer doesn't change on same-document
    // navigations.
    ExpectReferrerWithDefaultPolicy(entry, client_redirecting_url);
  }
}

// Checks the contents of the redirect chain after client-side redirect within
// the same document.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_MainFrameRedirectChain_ClientRedirectSameDocThenFragment) {
  NavigationControllerImpl& controller = contents()->GetController();

  // Navigate the main frame to a redirecting URL (client-side) that will
  // redirect to the same document as the original URL.
  GURL client_redirecting_url(embedded_test_server()->GetURL(
      "/navigation_controller/client_redirect_fragment.html"));
  GURL client_redirect_target_url(embedded_test_server()->GetURL(
      "/navigation_controller/client_redirect_fragment.html#foo"));

  {
    // Client-side redirects will result in a new navigation, so wait for two
    // navigations to finish.
    TestNavigationManager navigation_manager_1(contents(),
                                               client_redirecting_url);
    TestNavigationManager navigation_manager_2(contents(),
                                               client_redirect_target_url);
    shell()->LoadURL(client_redirecting_url);
    ASSERT_TRUE(navigation_manager_1
                    .WaitForNavigationFinished());  // Initial navigation.
    ASSERT_TRUE(navigation_manager_2
                    .WaitForNavigationFinished());  // Client-side redirect.

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetEntryAtIndex(0);
    ASSERT_EQ(client_redirect_target_url, entry->GetURL());

    // When a client-side redirect happens after a navigation, the redirect
    // chain contains the URL that triggers the client redirect first, then the
    // URL that we got redirected to.
    EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
    EXPECT_EQ(entry->GetRedirectChain()[0], client_redirecting_url);
    EXPECT_EQ(entry->GetRedirectChain()[1], client_redirect_target_url);

    // The client-redirecting URL is saved as the "replaced entry".
    ASSERT_TRUE(entry->GetReplacedEntryData().has_value());
    EXPECT_EQ(client_redirecting_url,
              entry->GetReplacedEntryData()->first_committed_url);

    // The original request URL will be the first entry of redirect chain,
    // which is the URL that initiated the client redirect.
    EXPECT_EQ(entry->GetOriginalRequestURL(), client_redirecting_url);

    // No referrer since the navigation started as a browser-initiated
    // navigation, and the client redirect that happened afterwards is a
    // same-document navigation, which won't change the referrer.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }

  {
    // Renderer-initiated fragment navigation that does replacement.
    GURL fragment_url(embedded_test_server()->GetURL(
        "/navigation_controller/client_redirect_fragment.html#bar"));
    TestNavigationManager navigation_manager(contents(), fragment_url);
    EXPECT_TRUE(ExecJs(contents(), "location.replace('#bar');"));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());

    EXPECT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(fragment_url, entry->GetURL());

    // When a renderer-initiated fragment navigation that does replacement
    // happens after a client-side redirect navigation,
    // the redirect chain will contain the previous URL
    // (the URL we ended up in after client redirect), then the fragment URL.
    EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
    EXPECT_EQ(entry->GetRedirectChain()[0], client_redirect_target_url);
    EXPECT_EQ(entry->GetRedirectChain()[1], fragment_url);

    EXPECT_TRUE(entry->GetReplacedEntryData().has_value());
    EXPECT_EQ(client_redirecting_url,
              entry->GetReplacedEntryData()->first_committed_url);

    // The original request URL will be the first entry of redirect chain,
    // which is the URL that initiated the client redirect.
    // It should be client_redirect_target_url since that's what triggered
    // the replacement. But, because this is a same-document replacement,
    // it goes through the EXISTING_ENTRY path which reuses the old entry
    // and doesn't set the original request URL with the new navigation's
    // original request URL.
    // TODO(https://crbug.com/1226489): Fix this.
    EXPECT_EQ(entry->GetOriginalRequestURL(), client_redirecting_url);

    // No referrer (same as the referrer used for loading the document) since
    // the refererrer doesn't change on same-document navigations.
    ExpectReferrerWithDefaultPolicy(entry, GURL());
  }
}

// Checks the contents of the redirect chain after a client-side redirect that
// happens after a server-side redirect.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_MainFrameRedirectChain_ServerThenClientRedirect) {
  NavigationControllerImpl& controller = contents()->GetController();

  // Navigate the main frame to a redirecting URL (server-side then client-side)
  GURL server_redirecting_url(embedded_test_server()->GetURL(
      "/server-redirect?/navigation_controller/client_redirect.html"));
  GURL client_redirecting_url(embedded_test_server()->GetURL(
      "/navigation_controller/client_redirect.html"));
  GURL final_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  {
    TestNavigationManager navigation_manager_1(contents(),
                                               server_redirecting_url);
    TestNavigationManager navigation_manager_2(contents(), final_url);

    shell()->LoadURL(server_redirecting_url);

    ASSERT_TRUE(navigation_manager_1
                    .WaitForNavigationFinished());  // Initial navigation.
    ASSERT_TRUE(navigation_manager_2
                    .WaitForNavigationFinished());  // Client-side redirect.

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetEntryAtIndex(0);
    ASSERT_EQ(final_url, entry->GetURL());

    // The redirect chain only contains the URL that triggers the client
    // redirect and the URL that we got redirected to. It does not contain the
    // server-redirecting URL.
    EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
    EXPECT_EQ(entry->GetRedirectChain()[0], client_redirecting_url);
    EXPECT_EQ(entry->GetRedirectChain()[1], final_url);

    // The client-redirecting URL is saved as the "replaced entry".
    ASSERT_TRUE(entry->GetReplacedEntryData().has_value());
    EXPECT_EQ(client_redirecting_url,
              entry->GetReplacedEntryData()->first_committed_url);

    // The original request URL will be the first entry of redirect chain,
    // which is the URL that initiated the client redirect.
    EXPECT_EQ(entry->GetOriginalRequestURL(), client_redirecting_url);

    // The referrer is |client_redirecting_url| since the final navigation is a
    // client-side redirect.
    ExpectReferrerWithDefaultPolicy(entry, client_redirecting_url);
  }
}

// Checks the contents of the redirect chain after a server-side redirect that
// happens after a client-side redirect.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    FrameNavigationEntry_MainFrameRedirectChain_ClientThenServerRedirect) {
  NavigationControllerImpl& controller = contents()->GetController();

  // Navigate the main frame to a redirecting URL (client-side then server-side)
  GURL client_redirecting_url(embedded_test_server()->GetURL(
      "/navigation_controller/client_redirect_server.html"));
  GURL server_redirecting_url(embedded_test_server()->GetURL(
      "/server-redirect?/navigation_controller/simple_page_1.html"));
  GURL final_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  {
    TestNavigationManager navigation_manager_1(contents(),
                                               client_redirecting_url);
    TestNavigationManager navigation_manager_2(contents(),
                                               server_redirecting_url);

    shell()->LoadURL(client_redirecting_url);

    ASSERT_TRUE(navigation_manager_1
                    .WaitForNavigationFinished());  // Initial navigation +
                                                    // client-side redirect.
    ASSERT_TRUE(navigation_manager_2
                    .WaitForNavigationFinished());  // Server-side redirect.

    ASSERT_EQ(1, controller.GetEntryCount());
    NavigationEntry* entry = controller.GetEntryAtIndex(0);
    ASSERT_EQ(final_url, entry->GetURL());

    // The redirect chain will contain the URL that triggers the client
    // redirect, then the server-redirecting URL (the target of the
    // client-redirector URL) and finally the final URL (the target of the
    // server-redirector URL).
    EXPECT_EQ(entry->GetRedirectChain().size(), 3u);
    EXPECT_EQ(entry->GetRedirectChain()[0], client_redirecting_url);
    EXPECT_EQ(entry->GetRedirectChain()[1], server_redirecting_url);
    EXPECT_EQ(entry->GetRedirectChain()[2], final_url);

    // The client-redirecting URL is saved as the "replaced entry".
    ASSERT_TRUE(entry->GetReplacedEntryData().has_value());
    EXPECT_EQ(client_redirecting_url,
              entry->GetReplacedEntryData()->first_committed_url);

    // The original request URL will be the first entry of redirect chain,
    // which is the URL that initiated the client redirect.
    EXPECT_EQ(entry->GetOriginalRequestURL(), client_redirecting_url);

    // The referrer is |client_redirecting_url| since the navigation started
    // with a client-side redirect, and a same-origin server redirect does not
    // affect a request's referrer.
    ExpectReferrerWithDefaultPolicy(entry, client_redirecting_url);
  }
}

// Verify that restoring a NavigationEntry with cross-site subframes does not
// create out-of-process iframes unless the current SiteIsolationPolicy says to.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RestoreWithoutExtraOopifs) {
  // 1. Start on a page with a data URL iframe.
  GURL main_url_a(embedded_test_server()->GetURL(
      "a.com", "/navigation_controller/page_with_data_iframe.html"));
  GURL data_url("data:text/html,Subframe");
  EXPECT_TRUE(NavigateToURL(shell(), main_url_a));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->current_url());

  // 2. Navigate the iframe cross-site.
  GURL frame_url_b(embedded_test_server()->GetURL(
      "b.com", "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url_b));
  EXPECT_EQ(main_url_a, root->current_url());
  EXPECT_EQ(frame_url_b, root->child_at(0)->current_url());

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();

  // 3. Create a NavigationEntry with the same PageState as |entry2|.
  std::unique_ptr<NavigationEntryImpl> restored_entry =
      NavigationEntryImpl::FromNavigationEntry(
          NavigationController::CreateNavigationEntry(
              main_url_a, Referrer(), absl::nullopt /* initiator_origin= */,
              /* initiator_base_url= */ absl::nullopt,
              ui::PAGE_TRANSITION_RELOAD, false, std::string(),
              controller.GetBrowserContext(),
              nullptr /* blob_url_loader_factory */));
  EXPECT_EQ(0U, restored_entry->root_node()->children.size());
  std::unique_ptr<NavigationEntryRestoreContextImpl> context =
      std::make_unique<NavigationEntryRestoreContextImpl>();
  restored_entry->SetPageState(entry2->GetPageState(), context.get());

  // The entry should have no SiteInstance in the FrameNavigationEntry for the
  // b.com subframe.
  EXPECT_FALSE(
      restored_entry->root_node()->children[0]->frame_entry->site_instance());

  // 4. Restore the new entry in a new tab and verify the correct URLs load.
  std::vector<std::unique_ptr<NavigationEntry>> entries;
  entries.push_back(std::move(restored_entry));
  Shell* new_shell = Shell::CreateNewWindow(
      controller.GetBrowserContext(), GURL::EmptyGURL(), nullptr, gfx::Size());
  FrameTreeNode* new_root =
      static_cast<WebContentsImpl*>(new_shell->web_contents())
          ->GetPrimaryFrameTree()
          .root();
  NavigationControllerImpl& new_controller =
      static_cast<NavigationControllerImpl&>(
          new_shell->web_contents()->GetController());
  new_controller.Restore(entries.size() - 1, RestoreType::kRestored, &entries);
  ASSERT_EQ(0u, entries.size());
  {
    TestNavigationObserver restore_observer(new_shell->web_contents());
    new_controller.LoadIfNecessary();
    restore_observer.Wait();
  }
  ASSERT_EQ(1U, new_root->child_count());
  EXPECT_EQ(main_url_a, new_root->current_url());
  EXPECT_EQ(frame_url_b, new_root->child_at(0)->current_url());

  if (AreStrictSiteInstancesEnabled()) {
    EXPECT_NE(new_root->current_frame_host()->GetSiteInstance(),
              new_root->child_at(0)->current_frame_host()->GetSiteInstance());
  } else {
    // When strict SiteInstances are not enabled, the subframe should be in the
    // same SiteInstance as the parent because both sites get mapped to the
    // default SiteInstance.
    EXPECT_TRUE(new_root->current_frame_host()
                    ->GetSiteInstance()
                    ->IsDefaultSiteInstance());
    EXPECT_EQ(new_root->current_frame_host()->GetSiteInstance(),
              new_root->child_at(0)->current_frame_host()->GetSiteInstance());
  }
}

namespace {

// Loads |start_url|, then loads |stalled_url| which stalls. While the page is
// stalled, a same document navigation happens. Make sure that all the
// navigations are properly classified.
void DoReplaceStateWhilePending(Shell* shell,
                                const GURL& start_url,
                                const GURL& stalled_url,
                                const std::string& replace_state_filename) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell->web_contents()->GetController());

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Start with one page.
  EXPECT_TRUE(NavigateToURL(shell, start_url));

  // Have the user decide to go to a different page which is very slow.
  TestNavigationManager stalled_navigation(shell->web_contents(), stalled_url);
  controller.LoadURL(stalled_url, Referrer(), ui::PAGE_TRANSITION_LINK,
                     std::string());
  EXPECT_TRUE(stalled_navigation.WaitForRequestStart());

  // That should be the pending entry.
  NavigationEntryImpl* entry = controller.GetPendingEntry();
  ASSERT_NE(nullptr, entry);
  EXPECT_EQ(stalled_url, entry->GetURL());

  {
    // Now the existing page uses history.replaceState().
    FrameNavigateParamsCapturer capturer(root);
    capturer.set_wait_for_load(false);
    std::string script =
        "history.replaceState({}, '', '" + replace_state_filename + "')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();

    // The fact that there was a pending entry shouldn't interfere with the
    // classification.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());
  }
}

}  // namespace

IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    NavigationTypeClassification_On1SameDocumentToXWhile2Pending) {
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  DoReplaceStateWhilePending(shell(), url1, url2, "x");
}

IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    NavigationTypeClassification_On1SameDocumentTo2While2Pending) {
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  DoReplaceStateWhilePending(shell(), url1, url2, "simple_page_2.html");
}

IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    NavigationTypeClassification_On1SameDocumentToXWhile1Pending) {
  GURL url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  DoReplaceStateWhilePending(shell(), url, url, "x");
}

IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    NavigationTypeClassification_On1SameDocumentTo1While1Pending) {
  GURL url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  DoReplaceStateWhilePending(shell(), url, url, "simple_page_1.html");
}

// Ensure that a pending NavigationEntry for a different navigation doesn't
// cause a commit to be incorrectly treated as a replacement.
// See https://crbug.com/593153.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       OtherCommitDuringPendingEntryWithReplacement) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Load an initial page.
  GURL start_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  int entry_count = controller.GetEntryCount();
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(start_url, controller.GetLastCommittedEntry()->GetURL());

  // Start a cross-process navigation with replacement, which never completes.
  GURL foo_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/page_with_links.html"));
  TestNavigationManager stalled_navigation(shell()->web_contents(), foo_url);
  NavigationController::LoadURLParams params(foo_url);
  params.should_replace_current_entry = true;
  controller.LoadURLWithParams(params);
  EXPECT_TRUE(stalled_navigation.WaitForRequestStart());

  // That should be the pending entry.
  NavigationEntryImpl* entry = controller.GetPendingEntry();
  ASSERT_NE(nullptr, entry);
  EXPECT_EQ(foo_url, entry->GetURL());
  EXPECT_EQ(entry_count, controller.GetEntryCount());

  {
    // Now the existing page uses history.pushState() while the pending entry
    // for the other navigation still exists.
    FrameNavigateParamsCapturer capturer(root);
    capturer.set_wait_for_load(false);
    std::string script = "history.pushState({}, '', 'pushed')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());
  }

  // The same document navigation should not have replaced the previous entry.
  GURL push_state_url(
      embedded_test_server()->GetURL("/navigation_controller/pushed"));
  EXPECT_EQ(entry_count + 1, controller.GetEntryCount());
  EXPECT_EQ(push_state_url, controller.GetLastCommittedEntry()->GetURL());
  EXPECT_EQ(start_url, controller.GetEntryAtIndex(0)->GetURL());
}

// This test ensures that if we go back from a page that has a replaceState()
// call in the window.beforeunload function, we commit to the proper navigation
// entry. https://crbug.com/597239
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       BackFromPageWithReplaceStateInBeforeUnload) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  // With BackForwardCache, old frame doesn't fire beforeunload handlers as the
  // page is stored in BackForwardCache on navigation.
  controller.GetBackForwardCache().DisableForTesting(
      content::BackForwardCache::TEST_USES_UNLOAD_EVENT);

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Load an initial page.
  GURL start_url(embedded_test_server()->GetURL(
      "/navigation_controller/beforeunload_replacestate_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(start_url, controller.GetLastCommittedEntry()->GetURL());

  // Go to the second page.
  std::string script = "document.getElementById('thelink').click()";
  EXPECT_TRUE(ExecJs(root, script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

  // Go back to the first page, which never completes. The attempt to unload the
  // second page, though, causes it to do a replaceState().
  TestNavigationManager manager(shell()->web_contents(), start_url);
  controller.GoBack();
  EXPECT_TRUE(manager.WaitForRequestStart());

  // The navigation that just happened was the replaceState(), which should not
  // have changed the position into the navigation entry list. Make sure that
  // the pending navigation didn't confuse anything.
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
}

// Ensure the renderer process does not get confused about the current entry
// due to subframes and replaced entries.  See https://crbug.com/480201.
// TODO(creis): Re-enable for Site Isolation FYI bots: https://crbug.com/502317.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       PreventSpoofFromSubframeAndReplace) {
  // Start at an initial URL.
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  // Now go to a page with a real iframe.
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_data_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url2));

  // It is safe to obtain the root frame tree node here, as it doesn't change.
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(1U, root->child_count());
  ASSERT_NE(nullptr, root->child_at(0));

  {
    // Navigate in the iframe.
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_2.html"));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
  }

  {
    // Go back in the iframe.
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }

  {
    // Go forward in the iframe.
    TestNavigationObserver forward_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoForward();
    forward_load_observer.Wait();
  }

  GURL url3(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe.html"));
  {
    // location.replace() to cause an inert commit.
    TestNavigationObserver replace_load_observer(shell()->web_contents());
    std::string script = "location.replace('" + url3.spec() + "')";
    EXPECT_TRUE(ExecJs(root, script));
    replace_load_observer.Wait();
  }

  {
    // Go back to url2.
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();

    // Make sure the URL is correct for both the entry and the main frame, and
    // that the process hasn't been killed for showing a spoof.
    EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
    EXPECT_EQ(url2, shell()->web_contents()->GetLastCommittedURL());
    EXPECT_EQ(url2, root->current_url());
  }

  {
    // Go back to reset main frame entirely.
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
    EXPECT_EQ(url1, shell()->web_contents()->GetLastCommittedURL());
    EXPECT_EQ(url1, root->current_url());
  }

  {
    // Go forward.
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoForward();
    back_load_observer.Wait();
    EXPECT_EQ(url2, shell()->web_contents()->GetLastCommittedURL());
    EXPECT_EQ(url2, root->current_url());
  }

  {
    // Go forward to the replaced URL.
    TestNavigationObserver forward_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoForward();
    forward_load_observer.Wait();

    // Make sure the URL is correct for both the entry and the main frame, and
    // that the process hasn't been killed for showing a spoof.
    EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
    EXPECT_EQ(url3, shell()->web_contents()->GetLastCommittedURL());
    EXPECT_EQ(url3, root->current_url());
  }
}

// Ensure the renderer process does not get killed if the main frame URL's path
// changes when going back in a subframe, since this was possible after
// a replaceState in the main frame (thanks to https://crbug.com/373041).
// See https://crbug.com/486916.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SubframeBackFromReplaceState) {
  // Start at a page with a real iframe.
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_data_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  // It is safe to obtain the root frame tree node here, as it doesn't change.
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(1U, root->child_count());
  ASSERT_NE(nullptr, root->child_at(0));

  {
    // Navigate in the iframe.
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_2.html"));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
  }

  {
    // history.replaceState().
    FrameNavigateParamsCapturer capturer(root);
    std::string script = "history.replaceState({}, 'replaced', 'replaced')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
  }

  GURL replaced_url = shell()->web_contents()->GetLastCommittedURL();

  {
    // Go back in the iframe.
    TestNavigationObserver back_load_observer(shell()->web_contents());
    shell()->web_contents()->GetController().GoBack();
    back_load_observer.Wait();
  }

  // The replaceState() should not be reverted by the iframe back.
  EXPECT_EQ(replaced_url, shell()->web_contents()->GetLastCommittedURL());

  // Make sure the renderer process has not been killed.
  EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
}

// Ensure that a main frame replaceState remains in effect after a subframe back
// navigation, even after cloning the tab. Also ensure that
// FrameNavigationEntries are not shared across cloned tabs.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SubframeBackFromReplaceStateInClonedTab) {
  // Start at a page with a real iframe.
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_data_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  // It is safe to obtain the root frame tree node here, as it doesn't change.
  NavigationControllerImpl& original_controller =
      static_cast<NavigationControllerImpl&>(
          shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(1U, root->child_count());
  ASSERT_NE(nullptr, root->child_at(0));

  {
    // Navigate in the iframe.
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_2.html"));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
  }

  // Clone the tab without navigating it.
  std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone();
  WebContentsImpl* cloned_tab_impl =
      static_cast<WebContentsImpl*>(new_tab.get());
  NavigationControllerImpl& cloned_controller =
      static_cast<NavigationControllerImpl&>(cloned_tab_impl->GetController());
  EXPECT_TRUE(cloned_controller.IsInitialNavigation());
  EXPECT_TRUE(cloned_controller.NeedsReload());

  // The FrameNavigationEntries should not be shared across tabs, but they
  // should be shared among entries in each tab.
  ASSERT_EQ(2, original_controller.GetEntryCount());
  ASSERT_EQ(2, cloned_controller.GetEntryCount());
  NavigationEntryImpl* original_previous_entry =
      original_controller.GetEntryAtIndex(0);
  NavigationEntryImpl* original_current_entry =
      original_controller.GetEntryAtIndex(1);
  NavigationEntryImpl* cloned_previous_entry =
      cloned_controller.GetEntryAtIndex(0);
  NavigationEntryImpl* cloned_current_entry =
      cloned_controller.GetEntryAtIndex(1);
  EXPECT_NE(original_current_entry->root_node()->frame_entry.get(),
            cloned_current_entry->root_node()->frame_entry.get());
  EXPECT_NE(original_current_entry->root_node()->children[0]->frame_entry.get(),
            cloned_current_entry->root_node()->children[0]->frame_entry.get());
  EXPECT_EQ(original_previous_entry->root_node()->frame_entry.get(),
            original_current_entry->root_node()->frame_entry.get());
  EXPECT_EQ(cloned_previous_entry->root_node()->frame_entry.get(),
            cloned_current_entry->root_node()->frame_entry.get());

  {
    // history.replaceState() in the original tab.
    FrameNavigateParamsCapturer capturer(root);
    std::string script = "history.replaceState({}, 'replaced', 'replaced2')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
  }

  {
    // Go back in the iframe in the cloned tab.
    TestNavigationObserver back_load_observer(new_tab.get());
    new_tab->GetController().GoBack();
    back_load_observer.Wait();
  }

  // Make sure the renderer process has not been killed.
  FrameTreeNode* cloned_root = cloned_tab_impl->GetPrimaryFrameTree().root();
  EXPECT_TRUE(cloned_root->current_frame_host()->IsRenderFrameLive());

  // Only the cloned tab's NavigationEntry should have changed.
  EXPECT_EQ(original_current_entry,
            original_controller.GetLastCommittedEntry());
  EXPECT_EQ(cloned_previous_entry, cloned_controller.GetLastCommittedEntry());

  // The url should have changed in the original tab but not in the clone.
  EXPECT_EQ(cloned_previous_entry, cloned_controller.GetLastCommittedEntry());
  EXPECT_NE(original_previous_entry->root_node()->frame_entry->url(),
            cloned_previous_entry->root_node()->frame_entry->url());
}

// Test that location.replace in a subframe correctly replaces a shared
// FrameNavigationEntry in the current NavigationEntry rather than updating the
// shared FrameNavigationEntry across multiple NavigationEntries.
// See https://crbug.com/1515381.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SubframeBackFromSubframeLocationReplace) {
  // It is safe to obtain the root frame tree node here, as it doesn't change.
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // 1. Navigate to A(B(srcdoc_B)).
  GURL main_url(
      embedded_test_server()->GetURL("a.com", "/page_with_blank_iframe.html"));
  GURL middle_frame_url(embedded_test_server()->GetURL(
      "b.com", "/frame_tree/page_with_srcdoc_frame.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  ASSERT_EQ(1U, root->child_count());
  FrameTreeNode* child = root->child_at(0);
  {
    FrameNavigateParamsCapturer capturer(child);
    EXPECT_TRUE(NavigateToURLFromRenderer(child, middle_frame_url));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
  }
  // Verify the subframe URLs and srcdoc origin.
  ASSERT_EQ(1U, child->child_count());
  FrameTreeNode* grandchild = child->child_at(0);
  EXPECT_EQ(middle_frame_url,
            child->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(GURL(url::kAboutSrcdocURL),
            grandchild->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(url::Origin::Create(middle_frame_url),
            grandchild->current_frame_host()->GetLastCommittedOrigin());
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();

  // 2. Navigate innermost frame to about:blank_A, from the main frame.
  {
    FrameNavigateParamsCapturer capturer(grandchild);
    EXPECT_TRUE(ExecJs(root, "frames[0][0].location = 'about:blank';"));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
  }
  EXPECT_EQ(GURL(url::kAboutBlankURL),
            grandchild->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(url::Origin::Create(main_url),
            grandchild->current_frame_host()->GetLastCommittedOrigin());
  // The middle frame's FrameNavigationEntry should be shared across the two
  // NavigationEntries.
  EXPECT_EQ(2, controller.GetEntryCount());
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();
  EXPECT_NE(entry1, entry2);
  EXPECT_EQ(entry1->GetFrameEntry(child), entry2->GetFrameEntry(child));
  EXPECT_NE(entry1->GetFrameEntry(grandchild),
            entry2->GetFrameEntry(grandchild));

  // 3. Location.replace in middle frame, to A(A(about:blank_A)).
  {
    FrameNavigateParamsCapturer capturer(child);
    EXPECT_TRUE(
        ExecJs(root, JsReplace("frames[0].location.replace($1)", main_url)));
    capturer.Wait();
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
  }
  // There's a new grandchild FrameTreeNode at this point.
  grandchild = child->child_at(0);
  EXPECT_EQ(GURL(url::kAboutBlankURL),
            grandchild->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(url::Origin::Create(main_url),
            grandchild->current_frame_host()->GetLastCommittedOrigin());
  // The FrameNavigationEntry in the middle frame should no longer be shared.
  EXPECT_NE(entry1->GetFrameEntry(child), entry2->GetFrameEntry(child));

  // 4. Go back.
  {
    TestNavigationObserver observer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, "history.back()"));
    observer.Wait();
  }

  // There's a new grandchild FrameTreeNode again at this point.
  grandchild = child->child_at(0);

  // Ensure the middle and innermost frames both navigated.
  EXPECT_EQ(middle_frame_url,
            child->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(GURL(url::kAboutSrcdocURL),
            grandchild->current_frame_host()->GetLastCommittedURL());

  // Ensure the innermost frame ends up back in B's origin and process.
  EXPECT_EQ(url::Origin::Create(middle_frame_url),
            grandchild->current_frame_host()->GetLastCommittedOrigin());
  EXPECT_EQ(child->current_frame_host()->GetProcess(),
            grandchild->current_frame_host()->GetProcess());
  if (AreAllSitesIsolatedForTesting()) {
    EXPECT_NE(root->current_frame_host()->GetProcess(),
              grandchild->current_frame_host()->GetProcess());
  }

  // Make sure the renderer processes have not been killed.
  EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
  EXPECT_TRUE(child->current_frame_host()->IsRenderFrameLive());
}

// Similar to SubframeBackFromSubframeLocationReplace test, but covers a tricky
// early RFH swap case if the renderer process crashes before location.replace.
// See https://crbug.com/1515381.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SubframeBackFromSubframeLocationReplaceAfterCrash) {
  // This tests something that can only happen with out of process iframes.
  if (!AreAllSitesIsolatedForTesting()) {
    return;
  }

  // It is safe to obtain the root frame tree node here, as it doesn't change.
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // 1. Navigate to A(B(srcdoc_B)).
  GURL main_url(
      embedded_test_server()->GetURL("a.com", "/page_with_blank_iframe.html"));
  GURL middle_frame_url(embedded_test_server()->GetURL(
      "b.com", "/frame_tree/page_with_srcdoc_frame.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  ASSERT_EQ(1U, root->child_count());
  FrameTreeNode* child = root->child_at(0);
  {
    FrameNavigateParamsCapturer capturer(child);
    EXPECT_TRUE(NavigateToURLFromRenderer(child, middle_frame_url));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
  }
  // Verify the subframe URLs and srcdoc origin.
  ASSERT_EQ(1U, child->child_count());
  FrameTreeNode* grandchild = child->child_at(0);
  EXPECT_EQ(middle_frame_url,
            child->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(GURL(url::kAboutSrcdocURL),
            grandchild->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(url::Origin::Create(middle_frame_url),
            grandchild->current_frame_host()->GetLastCommittedOrigin());
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();

  // 2. Navigate innermost frame to about:blank_A, from the main frame.
  {
    FrameNavigateParamsCapturer capturer(grandchild);
    EXPECT_TRUE(ExecJs(root, "frames[0][0].location = 'about:blank';"));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
  }
  EXPECT_EQ(GURL(url::kAboutBlankURL),
            grandchild->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(url::Origin::Create(main_url),
            grandchild->current_frame_host()->GetLastCommittedOrigin());
  // The middle frame's FrameNavigationEntry should be shared across the two
  // NavigationEntries.
  EXPECT_EQ(2, controller.GetEntryCount());
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();
  EXPECT_NE(entry1, entry2);
  EXPECT_EQ(entry1->GetFrameEntry(child), entry2->GetFrameEntry(child));
  EXPECT_NE(entry1->GetFrameEntry(grandchild),
            entry2->GetFrameEntry(grandchild));

  // Simulate a renderer process crash in the child frame, so that the next
  // navigation in this frame will trigger an early RFH swap.
  // TODO(https://crbug.com/1072817): This test may become unnecessary if we
  // stop doing early RFH swaps for crashed renderer processes.
  RenderProcessHost* process_b = child->current_frame_host()->GetProcess();
  RenderProcessHostWatcher crash_observer(
      process_b, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  EXPECT_TRUE(process_b->Shutdown(RESULT_CODE_KILLED));
  crash_observer.Wait();

  // 3. Location.replace in middle frame, to A(A(about:blank_A)).
  {
    FrameNavigateParamsCapturer capturer(child);
    EXPECT_TRUE(
        ExecJs(root, JsReplace("frames[0].location.replace($1)", main_url)));
    capturer.Wait();
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
  }
  // There's a new grandchild FrameTreeNode at this point.
  grandchild = child->child_at(0);
  EXPECT_EQ(GURL(url::kAboutBlankURL),
            grandchild->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(url::Origin::Create(main_url),
            grandchild->current_frame_host()->GetLastCommittedOrigin());
  // The FrameNavigationEntry in the middle frame should no longer be shared.
  EXPECT_NE(entry1->GetFrameEntry(child), entry2->GetFrameEntry(child));

  // 4. Go back.
  {
    TestNavigationObserver observer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, "history.back()"));
    observer.Wait();
  }

  // There's a new grandchild FrameTreeNode again at this point.
  grandchild = child->child_at(0);

  // Ensure the middle and innermost frames both navigated.
  EXPECT_EQ(middle_frame_url,
            child->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(GURL(url::kAboutSrcdocURL),
            grandchild->current_frame_host()->GetLastCommittedURL());

  // Ensure the innermost frame ends up back in B's origin and process.
  EXPECT_EQ(url::Origin::Create(middle_frame_url),
            grandchild->current_frame_host()->GetLastCommittedOrigin());
  EXPECT_EQ(child->current_frame_host()->GetProcess(),
            grandchild->current_frame_host()->GetProcess());
  if (AreAllSitesIsolatedForTesting()) {
    EXPECT_NE(root->current_frame_host()->GetProcess(),
              grandchild->current_frame_host()->GetProcess());
  }

  // Make sure the renderer processes have not been killed.
  EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
  EXPECT_TRUE(child->current_frame_host()->IsRenderFrameLive());
}

// Test that shared FrameNavigationEntries with opaque initiator origins can be
// restored in future sessions, even if the internal nonce is not preserved.
// See https://crbug.com/1354634.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RestoreSharedFrameEntryWithOpaqueInitiator) {
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // 1. Start at a data: URL with an iframe, giving the iframe an opaque
  // initiator origin.
  GURL data_url("data:text/html,<iframe></iframe>");
  EXPECT_TRUE(NavigateToURL(shell(), data_url));
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();
  FrameTreeNode* child = root->child_at(0);
  ASSERT_TRUE(entry1->GetFrameEntry(child)->initiator_origin()->opaque());

  // 2. Navigate to a fragment, staying in the same document. This shares the
  // iframe's FrameNavigationEntry from entry1.
  GURL data_url_fragment("data:text/html,<iframe></iframe>#foo");
  EXPECT_TRUE(NavigateToURL(shell(), data_url_fragment));
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();
  ASSERT_EQ(entry1->GetFrameEntry(child), entry2->GetFrameEntry(child));

  // 3. Simulate session restore by cloning the NavigationEntries but calling
  // SetPageState with saved PageStates from the entries, using a shared
  // RestoreContext. The second SetPageState call used to fail a DCHECK that the
  // initiator origin of the de-duplicated FrameNavigationEntries should match,
  // because the internal nonce of the opaque origin isn't preserved. See
  // https://crbug.com/1354634.
  std::unique_ptr<NavigationEntryRestoreContextImpl> context =
      std::make_unique<NavigationEntryRestoreContextImpl>();
  std::unique_ptr<NavigationEntryImpl> restored_entry1 = entry1->Clone();
  restored_entry1->SetPageState(entry1->GetPageState(), context.get());
  std::unique_ptr<NavigationEntryImpl> restored_entry2 = entry2->Clone();
  restored_entry2->SetPageState(entry2->GetPageState(), context.get());
  std::vector<std::unique_ptr<NavigationEntry>> restored_entries;
  restored_entries.push_back(std::move(restored_entry1));
  restored_entries.push_back(std::move(restored_entry2));

  // 4. Create a new tab and restore the entries, confirming that it works.
  Shell* new_shell = Shell::CreateNewWindow(
      controller.GetBrowserContext(), GURL::EmptyGURL(), nullptr, gfx::Size());
  WebContentsImpl* new_web_contents =
      static_cast<WebContentsImpl*>(new_shell->web_contents());
  NavigationControllerImpl& new_controller =
      static_cast<NavigationControllerImpl&>(new_web_contents->GetController());
  new_controller.Restore(restored_entries.size() - 1, RestoreType::kRestored,
                         &restored_entries);
  {
    TestNavigationObserver restore_observer(new_shell->web_contents());
    new_controller.LoadIfNecessary();
    restore_observer.Wait();
  }
  NavigationEntryImpl* new_entry2 = new_controller.GetEntryAtIndex(1);
  EXPECT_EQ(new_entry2, new_controller.GetLastCommittedEntry());
}

// Test that duplicate FrameNavigationEntries from pre-M93 can diverge in URL
// and other values (e.g., initiator origin), and will only be de-duplicated if
// their URLs, item sequence numbers, and targets match. See
// https://crbug.com/1275257. Also tests that we do not fail a DCHECK if
// remaining values like initiator origin still differ when de-duplicating them,
// per https://crbug.com/1354634.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RestoreNonSharedFrameEntryWithCrossOriginInitiator) {
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // 1. Visit an a.com URL.
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();

  // 2. Do a renderer-initiated navigation to a cross-origin URL, so that the
  // initiator origin is cross-origin.
  GURL url2(embedded_test_server()->GetURL(
      "b.com", "/navigation_controller/page_with_iframe_simple.html"));
  EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url2));
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();
  url::Origin initiator_origin2 =
      entry2->GetFrameEntry(root)->initiator_origin().value();
  ASSERT_EQ(initiator_origin2, url::Origin::Create(url1));

  // Serialize the state of `entry1` and `entry2` now, before `entry2` starts
  // sharing its main frame FNE in the next step. This simulates a pre-M93 saved
  // session where FNEs were not shared.  (See https://crbug.com/1211683).
  blink::PageState entry1_state = entry1->GetPageState();
  blink::PageState entry2_state = entry2->GetPageState();

  // 3. Navigate the subframe to create a shared FrameNavigationEntry in the
  // main frame.
  GURL frame_url(embedded_test_server()->GetURL(
      "b.com", "/navigation_controller/simple_page_2.html"));
  FrameTreeNode* child = root->child_at(0);
  {
    FrameNavigateParamsCapturer capturer(child);
    ASSERT_TRUE(NavigateFrameToURL(child, frame_url));
    capturer.Wait();
  }
  NavigationEntryImpl* entry3 = controller.GetLastCommittedEntry();

  // 4. Do a replaceState in the main frame, changing the initiator origin to
  // the current origin.
  GURL url2_fragment(url2.spec() + "#foo");
  {
    FrameNavigateParamsCapturer capturer(root);
    std::string script =
        "history.replaceState({}, 'replaced', '" + url2_fragment.spec() + "')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
  }
  url::Origin initiator_origin3 =
      entry3->GetFrameEntry(root)->initiator_origin().value();
  ASSERT_EQ(initiator_origin3, url::Origin::Create(url2));
  EXPECT_NE(initiator_origin3, initiator_origin2);

  // Serialize the state of `entry3` now, to simulate a diverging main-frame FNE
  // from pre-M93.
  blink::PageState entry3_state = entry3->GetPageState();

  // Simulate session restore in a post-M93 version by cloning the
  // NavigationEntries but calling SetPageState with the previously saved
  // PageStates, using a shared RestoreContext. This attempts to de-duplicate
  // FNEs if they match by item sequence number, URL, and target.
  std::unique_ptr<NavigationEntryRestoreContextImpl> context1 =
      std::make_unique<NavigationEntryRestoreContextImpl>();
  std::unique_ptr<NavigationEntryImpl> restored_entry1 = entry1->Clone();
  restored_entry1->SetPageState(entry1_state, context1.get());
  std::unique_ptr<NavigationEntryImpl> restored_entry2 = entry2->Clone();
  restored_entry2->SetPageState(entry2_state, context1.get());
  std::unique_ptr<NavigationEntryImpl> restored_entry3 = entry3->Clone();
  restored_entry3->SetPageState(entry3_state, context1.get());

  // The main frame FNE will not be de-duplicated, because the URL of the two
  // PageStates differs.  See https://crbug.com/1275257.
  EXPECT_NE(restored_entry2->GetFrameEntry(root),
            restored_entry3->GetFrameEntry(root));
  EXPECT_NE(restored_entry2->GetFrameEntry(root)->initiator_origin().value(),
            restored_entry3->GetFrameEntry(root)->initiator_origin().value());

  // 5. Actually restore the entries in a new tab.
  std::vector<std::unique_ptr<NavigationEntry>> restored_entries;
  restored_entries.push_back(std::move(restored_entry1));
  restored_entries.push_back(std::move(restored_entry2));
  restored_entries.push_back(std::move(restored_entry3));
  Shell* shell2 = Shell::CreateNewWindow(
      controller.GetBrowserContext(), GURL::EmptyGURL(), nullptr, gfx::Size());
  WebContentsImpl* web_contents2 =
      static_cast<WebContentsImpl*>(shell2->web_contents());
  FrameTreeNode* root2 = static_cast<WebContentsImpl*>(shell2->web_contents())
                             ->GetPrimaryFrameTree()
                             .root();
  NavigationControllerImpl& controller2 =
      static_cast<NavigationControllerImpl&>(web_contents2->GetController());
  controller2.Restore(restored_entries.size() - 1, RestoreType::kRestored,
                      &restored_entries);
  {
    TestNavigationObserver restore_observer(shell2->web_contents());
    controller2.LoadIfNecessary();
    restore_observer.Wait();
  }
  NavigationEntryImpl* new_entry1 = controller2.GetEntryAtIndex(0);
  NavigationEntryImpl* new_entry2 = controller2.GetEntryAtIndex(1);
  NavigationEntryImpl* new_entry3 = controller2.GetEntryAtIndex(2);
  EXPECT_EQ(new_entry3, controller2.GetLastCommittedEntry());

  // 6. Now do a replaceState in the main frame back to the original URL, such
  // that the FNEs can be de-duplicated on a future restore.
  {
    FrameNavigateParamsCapturer capturer(root2);
    std::string script = "history.replaceState({}, '', '" + url2.spec() + "')";
    EXPECT_TRUE(ExecJs(root2, script));
    capturer.Wait();
  }

  // The duplicate main frame FNEs should now have matching URLs and ISNs but
  // mismatched initiator origins.
  EXPECT_NE(new_entry2->GetFrameEntry(root2), new_entry3->GetFrameEntry(root2));
  EXPECT_EQ(new_entry2->GetFrameEntry(root2)->url(),
            new_entry3->GetFrameEntry(root2)->url());
  EXPECT_EQ(new_entry2->GetFrameEntry(root2)->item_sequence_number(),
            new_entry3->GetFrameEntry(root2)->item_sequence_number());
  EXPECT_NE(new_entry2->GetFrameEntry(root2)->initiator_origin().value(),
            new_entry3->GetFrameEntry(root2)->initiator_origin().value());

  // Simulate another session restore across GetPageState and SetPageState.
  // This time the main frame FNEs will be de-duplicated, despite not being
  // fully identical. This used to fail a DCHECK that the initiator origin of
  // de-duplicated FrameNavigationEntries should match. See
  // https://crbug.com/1354634.
  std::unique_ptr<NavigationEntryRestoreContextImpl> context2 =
      std::make_unique<NavigationEntryRestoreContextImpl>();
  std::unique_ptr<NavigationEntryImpl> restored2_entry1 = new_entry1->Clone();
  restored2_entry1->SetPageState(new_entry1->GetPageState(), context2.get());
  std::unique_ptr<NavigationEntryImpl> restored2_entry2 = new_entry2->Clone();
  restored2_entry2->SetPageState(new_entry2->GetPageState(), context2.get());
  std::unique_ptr<NavigationEntryImpl> restored2_entry3 = new_entry3->Clone();
  restored2_entry3->SetPageState(new_entry3->GetPageState(), context2.get());

  // The main frame FrameNavigationEntries should be shared in the new session.
  EXPECT_EQ(restored2_entry2->GetFrameEntry(root2),
            restored2_entry3->GetFrameEntry(root2));

  // 7. Actually restore the entries in a new tab.
  Shell* shell3 = Shell::CreateNewWindow(
      controller.GetBrowserContext(), GURL::EmptyGURL(), nullptr, gfx::Size());
  WebContentsImpl* web_contents3 =
      static_cast<WebContentsImpl*>(shell3->web_contents());
  FrameTreeNode* root3 = static_cast<WebContentsImpl*>(shell3->web_contents())
                             ->GetPrimaryFrameTree()
                             .root();
  NavigationControllerImpl& controller3 =
      static_cast<NavigationControllerImpl&>(web_contents3->GetController());
  std::vector<std::unique_ptr<NavigationEntry>> restored_entries2;
  restored_entries2.push_back(std::move(restored2_entry1));
  restored_entries2.push_back(std::move(restored2_entry2));
  restored_entries2.push_back(std::move(restored2_entry3));
  controller3.Restore(restored_entries2.size() - 1, RestoreType::kRestored,
                      &restored_entries2);
  {
    TestNavigationObserver restore_observer(shell3->web_contents());
    controller3.LoadIfNecessary();
    restore_observer.Wait();
  }

  // Verify that the main frame FNE is now shared.
  FrameNavigationEntry* root3_fne =
      controller3.GetEntryAtIndex(2)->GetFrameEntry(root3);
  EXPECT_EQ(root3_fne, controller3.GetEntryAtIndex(1)->GetFrameEntry(root3));
}

// Test that restoring a corrupt PageState can still successfully load the
// original URL without crashing, even if the rest of the PageState is lost.
// See https://crbug.com/1412401 and https://crbug.com/1196330, as well as
// SessionRestoreTest.RestoreInvalidPageState.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RestoreSessionWithInvalidPageState) {
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // 1. Navigate to a page with an iframe.
  GURL url1(embedded_test_server()->GetURL(
      "a.com", "/navigation_controller/page_with_iframe_simple.html"));
  GURL original_frame_url(embedded_test_server()->GetURL(
      "a.com", "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();
  FrameTreeNode* child = root->child_at(0);
  ASSERT_EQ(original_frame_url, entry1->GetFrameEntry(child)->url());

  // 2. Navigate the subframe.
  GURL frame_url(embedded_test_server()->GetURL(
      "b.com", "/navigation_controller/simple_page_2.html"));
  {
    FrameNavigateParamsCapturer capturer(child);
    ASSERT_TRUE(NavigateFrameToURL(child, frame_url));
    capturer.Wait();
  }
  NavigationEntryImpl* entry2 = controller.GetLastCommittedEntry();

  // Simulate a session restore using a corrupted PageState.  The other values
  // from the entry are preserved (simulated by cloning the entry).
  blink::PageState garbage = blink::PageState::CreateFromEncodedData("garbage");
  blink::ExplodedPageState exploded_state;
  ASSERT_FALSE(
      blink::DecodePageState(garbage.ToEncodedData(), &exploded_state));
  std::unique_ptr<NavigationEntryRestoreContextImpl> context1 =
      std::make_unique<NavigationEntryRestoreContextImpl>();
  std::unique_ptr<NavigationEntryImpl> restored_entry1 = entry1->Clone();
  restored_entry1->SetPageState(garbage, context1.get());
  std::unique_ptr<NavigationEntryImpl> restored_entry2 = entry2->Clone();
  restored_entry2->SetPageState(garbage, context1.get());

  // 3. Actually restore the entries in a new tab.
  std::vector<std::unique_ptr<NavigationEntry>> restored_entries;
  restored_entries.push_back(std::move(restored_entry1));
  restored_entries.push_back(std::move(restored_entry2));
  Shell* shell2 = Shell::CreateNewWindow(
      controller.GetBrowserContext(), GURL::EmptyGURL(), nullptr, gfx::Size());
  WebContentsImpl* web_contents2 =
      static_cast<WebContentsImpl*>(shell2->web_contents());
  FrameTreeNode* root2 = static_cast<WebContentsImpl*>(shell2->web_contents())
                             ->GetPrimaryFrameTree()
                             .root();
  NavigationControllerImpl& controller2 =
      static_cast<NavigationControllerImpl&>(web_contents2->GetController());
  controller2.Restore(restored_entries.size() - 1, RestoreType::kRestored,
                      &restored_entries);
  {
    TestNavigationObserver restore_observer(shell2->web_contents());
    controller2.LoadIfNecessary();
    restore_observer.Wait();
  }
  NavigationEntryImpl* new_entry1 = controller2.GetEntryAtIndex(0);
  NavigationEntryImpl* new_entry2 = controller2.GetEntryAtIndex(1);
  EXPECT_EQ(new_entry2, controller2.GetLastCommittedEntry());

  // Verify that the subframe loads its original URL, because frame_url was
  // lost in the corrupted PageState.
  FrameTreeNode* child2 = root2->child_at(0);
  FrameNavigationEntry* child2_fne = new_entry2->GetFrameEntry(child2);
  EXPECT_EQ(original_frame_url, child2_fne->url());

  // Ensure that the main frame entries do not share a FrameNavigationEntry,
  // despite ending up with identical item sequence numbers (i.e., 0) in the
  // corrupted PageState. This works because ISNs of 0 are exempt from the
  // de-deduplication logic.
  EXPECT_NE(new_entry1->GetFrameEntry(root2), new_entry2->GetFrameEntry(root2));
}

// Tests the value of history.state after same-document replacement, in all
// affected entries.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       StateAfterSameDocumentReplacement) {
  // Start at a page with an iframe.
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe_simple.html"));
  GURL url1_foo(url1.spec() + "#foo");
  GURL url1_bar(url1.spec() + "#bar");
  GURL frame_url2(embedded_test_server()->GetURL("/title2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  EXPECT_EQ(1, controller.GetEntryCount());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(1U, root->child_count());

  NavigationEntryImpl* previous_entry = controller.GetLastCommittedEntry();
  scoped_refptr<FrameNavigationEntry> previous_root_entry =
      previous_entry->root_node()->frame_entry.get();

  {
    // pushState "foo" in the main frame.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.pushState('foo', '', '#foo')"));
    capturer.Wait();
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_TRUE(capturer.is_same_document());
    // Current history state:
    //  [url1(frame_url1), *url1_foo(frame_url1)]
    EXPECT_EQ(url1_foo, contents()->GetLastCommittedURL());
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ(1, controller.GetCurrentEntryIndex());

    // The main frame's history.state is now "foo".
    EXPECT_EQ("foo", EvalJs(root, "history.state"));

    // The root FrameNavigationEntry and NavigationEntry are not reused.
    EXPECT_NE(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());

    previous_entry = controller.GetLastCommittedEntry();
    previous_root_entry = previous_entry->root_node()->frame_entry.get();
  }

  {
    // Navigate in the iframe. The main frame should stay the same.
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url2));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());

    // Current history state:
    //  [url1(frame_url1), url1_foo(frame_url1), *url1_foo(frame_url2)]
    EXPECT_EQ(url1_foo, contents()->GetLastCommittedURL());
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_EQ(2, controller.GetCurrentEntryIndex());

    // The main frame's history.state stays the same.
    EXPECT_EQ("foo", EvalJs(root, "history.state"));

    // The root FrameNavigationEntry is reused in the new NavigationEntry.
    EXPECT_EQ(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());

    previous_entry = controller.GetLastCommittedEntry();
    previous_root_entry = previous_entry->root_node()->frame_entry.get();
  }

  {
    // Do a location.replace() to a same-document URL in the main frame.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "location.replace('#bar');"));
    capturer.Wait();
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_TRUE(capturer.is_same_document());
    // This gets classified as EXISTING_ENTRY since it's a same-document
    // replacement. Note that a cross-document location.replace() would've been
    // classified as NEW_ENTRY instead.
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());

    EXPECT_EQ(url1_bar, contents()->GetLastCommittedURL());
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_EQ(2, controller.GetCurrentEntryIndex());
    // Current history state:
    //  [url1(frame_url1), url1_bar(frame_url1), *url1_bar(frame_url2)]
    // Notice the middle entry is updated to url1_bar too because the FNE is
    // shared with the replaced/updated entry.
    // TODO(https://crbug.com/1226489): Don't share the FNE so that we won't
    // update the middle entry.
    NavigationEntryImpl* current_entry = controller.GetLastCommittedEntry();
    EXPECT_EQ(url1_bar, current_entry->GetURL());
    EXPECT_EQ(url1_bar, controller.GetEntryAtIndex(1)->GetURL());
    EXPECT_NE(current_entry, controller.GetEntryAtIndex(1));
    EXPECT_EQ(current_entry->GetFrameEntry(root),
              controller.GetEntryAtIndex(1)->GetFrameEntry(root));

    // The main frame's history.state is reset
    EXPECT_EQ(nullptr, EvalJs(root, "history.state"));

    // The root FrameNavigationEntry and NavigationEntry are both reused. This
    // means the previous pushState FrameNavigationEntry is shared with the
    // current FrameNavigationEntry.
    EXPECT_EQ(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());

    previous_entry = controller.GetLastCommittedEntry();
    previous_root_entry = previous_entry->root_node()->frame_entry.get();
  }

  {
    // Go back. This will navigate the subframe.
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    controller.GoBack();
    capturer.Wait();
    // Current history state:
    //  [url1(frame_url1), *url1_bar(frame_url1), url1_bar(frame_url2)]
    // The main frame stays at the same URL, url1_bar (?!)
    EXPECT_EQ(url1_bar, contents()->GetLastCommittedURL());
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_EQ(1, controller.GetCurrentEntryIndex());

    // The main frame's history.state stays as null instead of being restored
    // to "foo", since the location.replace() call also affected other entries
    // that share the FrameNavigatonEntry.
    // TODO(https://crbug.com/1226489): We should probably restore "foo" here,
    // as location.replace() shouldn't affect entries other than the one it
    // replaced.
    EXPECT_EQ(nullptr, EvalJs(root, "history.state"));

    // The root FrameNavigationEntry is reused since it is shared with the
    // traversed NavigationEntry.
    EXPECT_EQ(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());

    previous_entry = controller.GetLastCommittedEntry();
    previous_root_entry = previous_entry->root_node()->frame_entry.get();
  }

  {
    // Go back. This will navigate the main frame.
    FrameNavigateParamsCapturer capturer(root);
    controller.GoBack();
    capturer.Wait();
    EXPECT_TRUE(capturer.is_same_document());

    // Current history state:
    //  [*url1(frame_url1), url1_bar(frame_url1), url1_bar(frame_url2)]
    // The main frame is back to url1.
    EXPECT_EQ(url1, contents()->GetLastCommittedURL());
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_TRUE(capturer.is_same_document());

    // The main frame's history.state stays as null.
    EXPECT_EQ(nullptr, EvalJs(root, "history.state"));

    // The root FrameNavigationEntry and NavigationEntry are not reused.
    EXPECT_NE(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
  }
}

// Tests the value of history.state after cross-document replacement, in all
// affected entries.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       StateAfterCrossDocumentReplacement) {
  // Start at a page with an iframe.
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_data_iframe.html"));
  GURL url1_foo(url1.spec() + "#foo");
  GURL url2(embedded_test_server()->GetURL("/title2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  EXPECT_EQ(1, controller.GetEntryCount());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(1U, root->child_count());

  NavigationEntryImpl* previous_entry = controller.GetLastCommittedEntry();
  scoped_refptr<FrameNavigationEntry> previous_root_entry =
      previous_entry->root_node()->frame_entry.get();

  {
    // pushState "foo" in the main frame.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.pushState('foo', '', '#foo')"));
    capturer.Wait();
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_TRUE(capturer.is_same_document());

    // Current history state:
    //  [url1(frame_url1), *url1_foo(frame_url1)]
    EXPECT_EQ(url1_foo, contents()->GetLastCommittedURL());
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ(1, controller.GetCurrentEntryIndex());

    // The main frame's history.state is now "foo".
    EXPECT_EQ("foo", EvalJs(root, "history.state"));

    // The root FrameNavigationEntry and NavigationEntry are not reused.
    EXPECT_NE(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());

    previous_entry = controller.GetLastCommittedEntry();
    previous_root_entry = previous_entry->root_node()->frame_entry.get();
  }

  {
    // Navigate in the iframe.
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    GURL frame_url2(embedded_test_server()->GetURL("/title1.html"));
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url2));
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());

    // Current history state:
    //  [url1(frame_url1), url1_foo(frame_url1), *url1_foo(frame_url2)]
    EXPECT_EQ(url1_foo, contents()->GetLastCommittedURL());
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_EQ(2, controller.GetCurrentEntryIndex());

    // The main frame's history.state stays the same.
    EXPECT_EQ("foo", EvalJs(root, "history.state"));

    // The root FrameNavigationEntry is reused in the new NavigationEntry.
    EXPECT_EQ(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());

    previous_entry = controller.GetLastCommittedEntry();
    previous_root_entry = previous_entry->root_node()->frame_entry.get();
  }

  {
    // Do a location.replace() to a cross-document URL in the main frame.
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, JsReplace("location.replace($1);", url2)));
    capturer.Wait();
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_FALSE(capturer.is_same_document());

    // Current history state:
    //  [url1(frame_url1), url1_foo(frame_url1), *url2]
    EXPECT_EQ(url2, contents()->GetLastCommittedURL());
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_EQ(2, controller.GetCurrentEntryIndex());

    // The main frame's history.state is reset
    EXPECT_EQ(nullptr, EvalJs(root, "history.state"));

    // The root FrameNavigationEntry and NavigationEntry are not reused.
    EXPECT_NE(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());

    previous_entry = controller.GetLastCommittedEntry();
    previous_root_entry = previous_entry->root_node()->frame_entry.get();
  }

  {
    // Go back. This will navigate the main frame and recreate the subframe.
    FrameNavigateParamsCapturer capturer(root);
    controller.GoBack();
    capturer.Wait();
    EXPECT_FALSE(capturer.is_same_document());

    // Current history state:
    //  [url1(frame_url1), *url1_foo(frame_url1), url2]
    EXPECT_EQ(url1_foo, contents()->GetLastCommittedURL());
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_EQ(1, controller.GetCurrentEntryIndex());

    // The main frame's history.state is restored to "foo".
    EXPECT_EQ("foo", EvalJs(root, "history.state"));

    // The root FrameNavigationEntry and NavigationEntry are not reused.
    EXPECT_NE(
        previous_root_entry,
        controller.GetLastCommittedEntry()->root_node()->frame_entry.get());
    EXPECT_NE(previous_entry, controller.GetLastCommittedEntry());
  }
}

namespace {

class FailureWatcher : public WebContentsObserver {
 public:
  // Observes failure for the specified |node|.
  explicit FailureWatcher(FrameTreeNode* node)
      : WebContentsObserver(
            WebContents::FromRenderFrameHost(node->current_frame_host())),
        frame_tree_node_id_(node->frame_tree_node_id()),
        message_loop_runner_(new MessageLoopRunner) {}

  void Wait() { message_loop_runner_->Run(); }

 private:
  void DidFailLoad(RenderFrameHost* render_frame_host,
                   const GURL& validated_url,
                   int error_code) override {
    RenderFrameHostImpl* rfh =
        static_cast<RenderFrameHostImpl*>(render_frame_host);
    if (rfh->frame_tree_node()->frame_tree_node_id() != frame_tree_node_id_)
      return;

    message_loop_runner_->Quit();
  }

  void DidFinishNavigation(NavigationHandle* handle) override {
    if (handle->HasCommitted() ||
        handle->GetFrameTreeNodeId() != frame_tree_node_id_) {
      return;
    }

    message_loop_runner_->Quit();
  }

  // The id of the FrameTreeNode whose navigations to observe.
  int frame_tree_node_id_;

  // The MessageLoopRunner used to spin the message loop.
  scoped_refptr<MessageLoopRunner> message_loop_runner_;
};

}  // namespace

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       StopCausesFailureDespiteJavaScriptURL) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Start with a normal page.
  GURL url1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  // Have the user decide to go to a different page which will not commit.
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  TestNavigationManager stalled_navigation(shell()->web_contents(), url2);
  controller.LoadURL(url2, Referrer(), ui::PAGE_TRANSITION_LINK, std::string());
  EXPECT_TRUE(stalled_navigation.WaitForResponse());

  // That should be the pending entry.
  NavigationEntryImpl* entry = controller.GetPendingEntry();
  ASSERT_NE(nullptr, entry);
  EXPECT_EQ(url2, entry->GetURL());

  // Loading a JavaScript URL shouldn't affect the ability to stop.
  {
    FailureWatcher watcher(root);
    GURL js("javascript:(function(){})()");
    controller.LoadURL(js, Referrer(), ui::PAGE_TRANSITION_LINK, std::string());
    EXPECT_EQ(entry, controller.GetPendingEntry());
    EXPECT_TRUE(shell()->web_contents()->IsLoading());
    shell()->web_contents()->Stop();
    watcher.Wait();
    EXPECT_FALSE(shell()->web_contents()->IsLoading());
  }
}

namespace {
class RenderProcessKilledObserver : public WebContentsObserver {
 public:
  explicit RenderProcessKilledObserver(WebContents* web_contents)
      : WebContentsObserver(web_contents) {}
  ~RenderProcessKilledObserver() override {}

  void PrimaryMainFrameRenderProcessGone(
      base::TerminationStatus status) override {
    CHECK_NE(status,
             base::TerminationStatus::TERMINATION_STATUS_PROCESS_WAS_KILLED);
  }
};
}  // namespace

// This tests a race in Reload with ReloadType::ORIGINAL_REQUEST_URL, where a
// cross-origin reload was causing an in-flight replaceState to look like a
// cross-origin navigation, even though it's same document.  (The reload should
// not modify the underlying last committed entry.)  Not crashing means that
// the test is successful.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, ReloadOriginalRequest) {
  // TODO(lukasza): https://crbug.com/1159466: Get tests working for all
  // process model modes.
  if (AreStrictSiteInstancesEnabled() ||
      CanCrossSiteNavigationsProactivelySwapBrowsingInstances()) {
    return;
  }

  GURL original_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), original_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  RenderProcessKilledObserver kill_observer(shell()->web_contents());

  // Redirect so that we can use Reload with ReloadType::ORIGINAL_REQUEST_URL.
  GURL redirect_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_1.html"));
  {
    std::string script = "location.replace('" + redirect_url.spec() + "');";
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(shell(), script));
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
                                  ui::PAGE_TRANSITION_CLIENT_REDIRECT)));
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
  }

  // Modify an entry in the session history and reload the original request.
  {
    // We first send a replaceState() to the renderer, which will cause the
    // renderer to send back a DidCommitProvisionalLoad. Immediately after,
    // we send a Reload request with ReloadType::ORIGINAL_REQUEST_URL (which in
    // this case is a different origin) and will also cause the renderer to
    // commit the frame. In the end we verify that both navigations committed
    // and that the URLs are correct.
    std::string script = "history.replaceState({}, '', 'foo');";
    root->render_manager()
        ->current_frame_host()
        ->ExecuteJavaScriptWithUserGestureForTests(base::UTF8ToUTF16(script),
                                                   base::NullCallback());
    EXPECT_FALSE(shell()->web_contents()->IsLoading());
    shell()->web_contents()->GetController().Reload(
        ReloadType::ORIGINAL_REQUEST_URL, false);
    EXPECT_TRUE(shell()->web_contents()->IsLoading());
    EXPECT_EQ(redirect_url, shell()->web_contents()->GetLastCommittedURL());

    // Wait until there's no more navigations.
    GURL modified_url(embedded_test_server()->GetURL(
        "foo.com", "/navigation_controller/foo"));
    FrameNavigateParamsCapturer capturer(root);
    capturer.set_wait_for_load(false);
    capturer.set_navigations_remaining(2);
    capturer.Wait();
    EXPECT_EQ(2U, capturer.urls().size());
    EXPECT_EQ(modified_url, capturer.urls()[0]);
    EXPECT_EQ(original_url, capturer.urls()[1]);
    EXPECT_EQ(original_url, shell()->web_contents()->GetLastCommittedURL());
  }

  // Make sure the renderer is still alive.
  EXPECT_TRUE(ExecJs(shell(), "console.log('Success');"));
}

// This test shows that the initial "about:blank" URL in a subframe is replaced
// in all navigation entries when the subframe is navigated, even if a different
// frame has created a new navigation entry since then.
//
// It also prevents regression for an same document navigation renderer kill
// when going back after an in-page navigation in the main frame is followed by
// an auto subframe navigation, due to a bug in
// WebHistoryEntry::CloneAndReplace. See https://crbug.com/612713.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       InitialAboutBlankInIframeIsReplaced) {
  GURL original_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), original_url));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  NavigationController& controller = shell()->web_contents()->GetController();
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(1, EvalJs(shell(), "history.length"));

  // Add an iframe with no 'src'.

  std::string script =
      "var iframe = document.createElement('iframe');"
      "iframe.id = 'frame';"
      "document.body.appendChild(iframe);";
  EXPECT_TRUE(ExecJs(root, script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(1, EvalJs(shell(), "history.length"));
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());

  ASSERT_EQ(1U, root->child_count());
  FrameTreeNode* frame = root->child_at(0);
  ASSERT_NE(nullptr, frame);

  GURL blank_url(url::kAboutBlankURL);
  EXPECT_EQ(blank_url, frame->current_url());

  // Now create a new navigation entry. The old navigation entry and the new
  // navigation entry will share the same FrameNavigationEntry for the iframe,
  // which will still be at about blank.

  script = "history.pushState({}, '', 'notarealurl.html')";
  EXPECT_TRUE(ExecJs(root, script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(2, EvalJs(shell(), "history.length"));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

  // Load the iframe; the initial "about:blank" URL should be elided and thus we
  // shouldn't get a new navigation entry. Because the FrameNavigationEntry for
  // the iframe is shared, this will update the url for the iframe in both
  // navigation entries.

  GURL frame_url = embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_2.html");
  EXPECT_TRUE(NavigateToURLFromRenderer(frame, frame_url));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(2, EvalJs(shell(), "history.length"));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

  EXPECT_EQ(frame_url, frame->current_url());

  // Go back.
  {
    TestNavigationObserver observer(shell()->web_contents(), 1);
    ASSERT_TRUE(controller.CanGoBack());
    controller.GoBack();
    observer.Wait();
  }

  // The iframe's url should not have changed.
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(2, EvalJs(shell(), "history.length"));
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(frame_url, frame->current_url());

  // Now test for https://crbug.com/612713 to prevent an NC_IN_PAGE_NAVIGATION
  // renderer kill.

  // Do a same document navigation in the subframe.
  std::string fragment_script = "location.href = \"#foo\";";
  EXPECT_TRUE(ExecJs(frame->current_frame_host(), fragment_script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(2, EvalJs(shell(), "history.length"));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

  GURL frame_url_after_fragment_nav = embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_2.html#foo");
  EXPECT_EQ(frame_url_after_fragment_nav, frame->current_url());

  // Go back.
  {
    TestNavigationObserver observer(shell()->web_contents(), 1);
    controller.GoBack();
    observer.Wait();
  }

  // Verify the process is still alive by running script.  We can't just call
  // IsRenderFrameLive after the navigation since it might not have disconnected
  // yet.
  EXPECT_TRUE(ExecJs(root->current_frame_host(), "true;"));
  EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());

  // The back navigation should still return to the first non-empty url the
  // subframe had, not about:blank
  EXPECT_EQ(frame_url, frame->current_url());
}

// This test is similar to "BackToAboutBlankIframe" above, except that a
// fragment navigation is used rather than pushState (both create a same
// document navigation, so we need to test both), and an initial 'src' is given
// to the iframe to test proper restoration in that case.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       BackToIframeWithContent) {
  GURL links_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_links.html"));
  EXPECT_TRUE(NavigateToURL(shell(), links_url));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  NavigationController& controller = shell()->web_contents()->GetController();
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(1, EvalJs(shell(), "history.length"));

  // Add an iframe with a 'src'.

  GURL frame_url_1 = embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html");
  std::string script = JsReplace(
      "var iframe = document.createElement('iframe');"
      "iframe.src = $1;"
      "iframe.id = 'frame';"
      "document.body.appendChild(iframe);",
      frame_url_1);
  EXPECT_TRUE(ExecJs(root, script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(1, EvalJs(shell(), "history.length"));
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());

  ASSERT_EQ(1U, root->child_count());
  FrameTreeNode* frame = root->child_at(0);
  ASSERT_NE(nullptr, frame);

  EXPECT_EQ(frame_url_1, frame->current_url());

  // Do a fragment navigation, creating a new navigation entry. Note that the
  // old navigation entry has frame_url_1 as the URL in the iframe.

  script = "document.getElementById('fraglink').click()";
  EXPECT_TRUE(ExecJs(root, script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(2, EvalJs(shell(), "history.length"));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

  EXPECT_EQ(frame_url_1, frame->current_url());

  // Navigate the iframe; unlike the test "BackToAboutBlankIframe" above, this
  // _will_ create a new navigation entry.

  GURL frame_url_2 = embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_2.html");
  EXPECT_TRUE(NavigateToURLFromRenderer(frame, frame_url_2));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(3, EvalJs(shell(), "history.length"));
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());

  EXPECT_EQ(frame_url_2, frame->current_url());

  // Go back two entries.
  {
    TestNavigationObserver observer(shell()->web_contents(), 1);
    ASSERT_TRUE(controller.CanGoToOffset(-2));
    controller.GoToOffset(-2);
    observer.Wait();
  }

  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(3, EvalJs(shell(), "history.length"));
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(frame_url_1, frame->current_url());

  // Now test for https://crbug.com/612713 to prevent an NC_IN_PAGE_NAVIGATION
  // renderer kill.

  // Do a same document navigation in the subframe.
  std::string fragment_script = "location.href = \"#foo\";";
  EXPECT_TRUE(ExecJs(frame->current_frame_host(), fragment_script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(2, EvalJs(shell(), "history.length"));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

  // Go back.
  {
    TestNavigationObserver observer(shell()->web_contents(), 1);
    controller.GoBack();
    observer.Wait();
  }

  // Verify the process is still alive by running script.  We can't just call
  // IsRenderFrameLive after the navigation since it might not have disconnected
  // yet.
  EXPECT_TRUE(ExecJs(root->current_frame_host(), "true;"));
  EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());

  // TODO(creis): It's a bit surprising to go to frame_url_1 here instead of
  // frame_url_2.  Perhaps we should be going back to frame_url_1 when going
  // back two entries above, since it's different than the initial blank case.
  EXPECT_EQ(frame_url_1, frame->current_url());
}

// Test for same document navigation kills due to using the wrong history item
// in HistoryController::RecursiveGoToEntry and
// NavigationControllerImpl::FindFramesToNavigate.
// See https://crbug.com/612713.
//
// TODO(creis): Enable this test when https://crbug.com/618100 is fixed.
// Disabled for now while we switch to the new navigation path, since this kill
// is exceptionally rare in practice.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       DISABLED_BackTwiceToIframeWithContent) {
  GURL links_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_links.html"));
  EXPECT_TRUE(NavigateToURL(shell(), links_url));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  NavigationController& controller = shell()->web_contents()->GetController();
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(1, EvalJs(shell(), "history.length"));

  // Add an iframe with a 'src'.

  GURL frame_url_1 = embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html");
  std::string script = JsReplace(
      "var iframe = document.createElement('iframe');"
      "iframe.src = $1;"
      "iframe.id = 'frame';"
      "document.body.appendChild(iframe);",
      frame_url_1);
  EXPECT_TRUE(ExecJs(root->current_frame_host(), script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(1, EvalJs(shell(), "history.length"));
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());

  ASSERT_EQ(1U, root->child_count());
  FrameTreeNode* frame = root->child_at(0);
  ASSERT_NE(nullptr, frame);

  EXPECT_EQ(frame_url_1, frame->current_url());

  // Do a same document navigation in the subframe.
  GURL frame_url_2 = embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html#foo");
  std::string fragment_script = "location.href = \"#foo\";";
  EXPECT_TRUE(ExecJs(frame->current_frame_host(), fragment_script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(2, EvalJs(shell(), "history.length"));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(frame_url_2, frame->current_url());

  // Do a fragment navigation at the top level.
  std::string link_script = "document.getElementById('fraglink').click()";
  EXPECT_TRUE(ExecJs(root->current_frame_host(), link_script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(3, EvalJs(shell(), "history.length"));
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(frame_url_2, frame->current_url());

  // Go cross-site in the iframe.
  GURL frame_url_3 = embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_2.html");
  EXPECT_TRUE(NavigateToURLFromRenderer(frame, frame_url_3));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(4, EvalJs(shell(), "history.length"));
  EXPECT_EQ(3, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(frame_url_3, frame->current_url());

  // Go back two entries.
  {
    TestNavigationObserver observer(shell()->web_contents(), 1);
    ASSERT_TRUE(controller.CanGoToOffset(-2));
    controller.GoToOffset(-2);
    observer.Wait();
  }
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(4, EvalJs(shell(), "history.length"));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(links_url, root->current_url());
  EXPECT_EQ(frame_url_2, frame->current_url());

  // Now test for https://crbug.com/612713 to prevent an NC_IN_PAGE_NAVIGATION
  // renderer kill.

  // Go back.
  {
    TestNavigationObserver observer(shell()->web_contents(), 1);
    controller.GoBack();
    observer.Wait();
  }

  // Verify the process is still alive by running script.  We can't just call
  // IsRenderFrameLive after the navigation since it might not have disconnected
  // yet.
  EXPECT_TRUE(ExecJs(root->current_frame_host(), "true;"));
  EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());

  // TODO(creis): It's a bit surprising to go to frame_url_1 here instead of
  // frame_url_2.  Perhaps we should be going back to frame_url_1 when going
  // back two entries above, since it's different than the initial blank case.
  EXPECT_EQ(frame_url_1, frame->current_url());
}

// Test for same document navigation kills when going back to about:blank after
// a document.write.  See https://crbug.com/446959.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       BackAfterIframeDocumentWrite) {
  GURL links_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_links.html"));
  EXPECT_TRUE(NavigateToURL(shell(), links_url));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  NavigationController& controller = shell()->web_contents()->GetController();
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(1, EvalJs(shell(), "history.length"));

  // Add an iframe with no 'src'.
  GURL blank_url(url::kAboutBlankURL);
  std::string script =
      "var iframe = document.createElement('iframe');"
      "iframe.id = 'frame';"
      "document.body.appendChild(iframe);";
  EXPECT_TRUE(ExecJs(root->current_frame_host(), script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(1, EvalJs(shell(), "history.length"));
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  ASSERT_EQ(1U, root->child_count());
  FrameTreeNode* frame = root->child_at(0);
  ASSERT_NE(nullptr, frame);
  EXPECT_EQ(blank_url, frame->current_url());

  // Do a document.write in the subframe to create a link to click.
  std::string document_write_script =
      "var iframe = document.getElementById('frame');"
      "iframe.contentWindow.document.write("
      "    \"<a id='fraglink' href='#frag'>fragment link</a>\");"
      "iframe.contentWindow.document.close();";
  EXPECT_TRUE(ExecJs(root->current_frame_host(), document_write_script));

  // Click the link to do a same document navigation.  Due to the
  // document.write, the new URL matches the parent frame's URL.
  GURL frame_url_2(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_links.html#frag"));
  std::string link_script = "document.getElementById('fraglink').click()";
  EXPECT_TRUE(ExecJs(frame->current_frame_host(), link_script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(2, EvalJs(shell(), "history.length"));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(frame_url_2, frame->current_url());

  // Go back.
  {
    TestNavigationObserver observer(shell()->web_contents(), 1);
    controller.GoBack();
    observer.Wait();
  }

  // Verify the process is still alive by running script.  We can't just call
  // IsRenderFrameLive after the navigation since it might not have disconnected
  // yet.
  EXPECT_TRUE(ExecJs(root->current_frame_host(), "true;"));
  EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());

  EXPECT_EQ(blank_url, frame->current_url());
}

// Test for same document navigation kills when going back to about:blank in an
// iframe of a data URL, after a document.write.  This differs from
// BackAfterIframeDocumentWrite because both about:blank and the data URL are
// considered unique origins.  See https://crbug.com/446959.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       BackAfterIframeDocumentWriteInDataURL) {
  GURL data_url("data:text/html,Top level page");
  EXPECT_TRUE(NavigateToURL(shell(), data_url));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  NavigationController& controller = shell()->web_contents()->GetController();
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(1, EvalJs(shell(), "history.length"));
  const url::Origin opaque_origin = root->current_origin();
  EXPECT_TRUE(opaque_origin.opaque());
  EXPECT_EQ(url::SchemeHostPort(),
            opaque_origin.GetTupleOrPrecursorTupleIfOpaque());

  // Add an iframe with no 'src'.
  GURL blank_url(url::kAboutBlankURL);
  std::string script =
      "var iframe = document.createElement('iframe');"
      "iframe.id = 'frame';"
      "document.body.appendChild(iframe);";
  EXPECT_TRUE(ExecJs(root, script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(1, EvalJs(root, "history.length"));
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  ASSERT_EQ(1U, root->child_count());
  FrameTreeNode* frame = root->child_at(0);
  ASSERT_NE(nullptr, frame);
  EXPECT_EQ(blank_url, frame->current_url());
  EXPECT_EQ(opaque_origin, root->current_origin());
  EXPECT_EQ(opaque_origin, frame->current_origin());

  // Do a document.write in the subframe to create a link to click.
  std::string html = "<a id='fraglink' href='#frag'>fragment link</a>";
  std::string document_write_script = JsReplace(
      "var iframe = document.getElementById('frame');"
      "iframe.contentWindow.document.write($1);"
      "iframe.contentWindow.document.close();",
      html);
  EXPECT_TRUE(ExecJs(root, document_write_script));
  EXPECT_EQ(opaque_origin, root->current_origin());
  EXPECT_EQ(opaque_origin, frame->current_origin());

  // Click the link to do a same document navigation.  Due to the
  // document.write, the new URL matches the parent frame's URL, but the
  // opaque origin is preserved.
  GURL frame_url_2("data:text/html,Top level page#frag");
  std::string link_script = "document.getElementById('fraglink').click()";
  EXPECT_TRUE(ExecJs(frame, link_script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(opaque_origin, root->current_origin());
  EXPECT_EQ(opaque_origin, frame->current_origin());
  EXPECT_EQ(ListValueOf("Top level page", "fragment link"),
            EvalJs(frame,
                   "[window.parent.document.body.textContent,"
                   " document.body.textContent]"))
      << "Frames should be same-origin and able to script each other.";
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(2, EvalJs(root, "history.length"));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(frame_url_2, frame->current_url());

  // Go back.
  {
    TestNavigationObserver observer(shell()->web_contents(), 1);
    controller.GoBack();
    observer.Wait();
  }

  // Verify the process is still alive by running script.  We can't just call
  // IsRenderFrameLive after the navigation since it might not have disconnected
  // yet.
  EXPECT_EQ("ping", EvalJs(root, "'ping'"));
  EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());

  EXPECT_EQ(blank_url, frame->current_url());
  EXPECT_EQ(opaque_origin, frame->current_origin());
}

// Ensure that we do not corrupt a NavigationEntry's PageState if a subframe
// forward navigation commits after we've already started another forward
// navigation in the main frame.  See https://crbug.com/597322.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ForwardInSubframeWithPendingForward) {
  // The test expects history navigations to be not instant, so that it can
  // start another forward navigation while another forward navigation has
  // already started.
  DisableBackForwardCacheForTesting(shell()->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);
  // Navigate to a page with an iframe.
  GURL url_a(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_data_iframe.html"));
  GURL frame_url_a1("data:text/html,Subframe");
  EXPECT_TRUE(NavigateToURL(shell(), url_a));

  NavigationController& controller = shell()->web_contents()->GetController();
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(1U, root->child_count());
  EXPECT_EQ(url_a, root->current_url());
  FrameTreeNode* frame = root->child_at(0);
  EXPECT_EQ(frame_url_a1, frame->current_url());

  // Navigate the iframe to a second page.
  GURL frame_url_a2 = embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html");
  EXPECT_TRUE(NavigateToURLFromRenderer(frame, frame_url_a2));

  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_a, root->current_url());
  EXPECT_EQ(frame_url_a2, frame->current_url());

  // Navigate the top-level frame to another page with an iframe.
  GURL url_b(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe.html"));
  GURL frame_url_2(url::kAboutBlankURL);
  EXPECT_TRUE(NavigateToURL(shell(), url_b));
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(url_b, root->current_url());
  EXPECT_EQ(frame_url_2, root->child_at(0)->current_url());

  // Go back two entries. The original frame URL should be back.
  ASSERT_TRUE(controller.CanGoToOffset(-2));
  controller.GoToOffset(-2);
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_a, root->current_url());
  EXPECT_EQ(frame_url_a1, root->child_at(0)->current_url());

  // Go forward two times in a row, being careful that the subframe commits
  // after the second forward navigation begins but before the main frame
  // commits.
  FrameTestNavigationManager subframe_delayer(
      root->child_at(0)->frame_tree_node_id(), shell()->web_contents(),
      frame_url_a2);
  TestNavigationManager mainframe_delayer(shell()->web_contents(), url_b);
  controller.GoForward();
  EXPECT_TRUE(subframe_delayer.WaitForRequestStart());
  controller.GoForward();
  EXPECT_TRUE(mainframe_delayer.WaitForRequestStart());
  EXPECT_EQ(2, controller.GetPendingEntryIndex());

  // Let the subframe commit.
  ASSERT_TRUE(subframe_delayer.WaitForNavigationFinished());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_a, root->current_url());
  EXPECT_EQ(frame_url_a2, root->child_at(0)->current_url());

  // Let the main frame commit.
  ASSERT_TRUE(mainframe_delayer.WaitForNavigationFinished());
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_b, root->current_url());
  EXPECT_EQ(frame_url_2, root->child_at(0)->current_url());

  // Check the PageState of the previous entry to ensure it isn't corrupted.
  NavigationEntry* entry = controller.GetEntryAtIndex(1);
  EXPECT_EQ(url_a, entry->GetURL());
  blink::ExplodedPageState exploded_state;
  EXPECT_TRUE(blink::DecodePageState(entry->GetPageState().ToEncodedData(),
                                     &exploded_state));
  EXPECT_EQ(url_a,
            GURL(exploded_state.top.url_string.value_or(std::u16string())));
  EXPECT_EQ(frame_url_a2,
            GURL(exploded_state.top.children.at(0).url_string.value_or(
                std::u16string())));
}

// Start a provisional navigation, but abort it by going back before it commits.
// In crbug.com/631617 there was an issue which cleared the
// pending_navigation_params_ in RenderFrameImpl. This caused the interrupting
// navigation to lose important navigation data like its nav_entry_id, which
// could cause it to commit in-place instead of in the correct location in the
// browsing history.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       AbortProvisionalLoadRetainsNavigationParams) {
  EXPECT_TRUE(
      NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
  EXPECT_TRUE(
      NavigateToURL(shell(), embedded_test_server()->GetURL("/title2.html")));

  TestNavigationManager delayer(shell()->web_contents(),
                                embedded_test_server()->GetURL("/title3.html"));
  shell()->LoadURL(embedded_test_server()->GetURL("/title3.html"));
  EXPECT_TRUE(delayer.WaitForRequestStart());

  NavigationController& controller = shell()->web_contents()->GetController();
  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_TRUE(controller.CanGoForward());
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());
}

// Make sure that a 304 response to a navigation aborts the navigation.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, NavigateTo304) {
  // URL that just returns a blank page.
  GURL initial_url = embedded_test_server()->GetURL("/set-header");
  // URL that returns a response with a 304 status code.
  GURL not_modified_url = embedded_test_server()->GetURL("/echo?status=304");

  EXPECT_TRUE(NavigateToURL(shell(), initial_url));
  EXPECT_EQ(initial_url, shell()->web_contents()->GetVisibleURL());

  // The navigation should be aborted.
  EXPECT_FALSE(NavigateToURL(shell(), not_modified_url));
  EXPECT_EQ(initial_url, shell()->web_contents()->GetVisibleURL());
}

// Ensure that we do not corrupt a NavigationEntry's PageState if two forward
// navigations compete in different frames.  See https://crbug.com/623319.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       PageStateAfterForwardInCompetingFrames) {
  // This test might trigger the "undo commit" path in the renderer, that does
  // not work with RenderDocument, causing flakiness.
  // TODO(https://crbug.com/1220337): Fix this.
  if (ShouldCreateNewHostForSameSiteSubframe())
    return;
  // Navigate to a page with an iframe.
  GURL url_a(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_data_iframe.html"));
  GURL frame_url_a1("data:text/html,Subframe");
  EXPECT_TRUE(NavigateToURL(shell(), url_a));

  NavigationController& controller = shell()->web_contents()->GetController();
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_EQ(url_a, root->current_url());
  EXPECT_EQ(frame_url_a1, root->child_at(0)->current_url());

  // Navigate the iframe to a second page.
  GURL frame_url_a2 = embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html");
  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url_a2));

  // Navigate the iframe to about:blank.
  GURL blank_url(url::kAboutBlankURL);
  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), blank_url));
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_a, root->current_url());
  EXPECT_EQ(blank_url, root->child_at(0)->current_url());

  // Go back to the middle entry.
  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

  // Replace the entry with a cross-site top-level page.  By doing a
  // replacement, the main frame pages before and after have the same item
  // sequence number, and thus going between them only requires loads in the
  // subframe.
  GURL url_b(embedded_test_server()->GetURL(
      "b.com", "/navigation_controller/simple_page_2.html"));
  std::string replace_script = "location.replace('" + url_b.spec() + "')";
  TestNavigationObserver replace_observer(shell()->web_contents());
  EXPECT_TRUE(ExecJs(shell()->web_contents(), replace_script));
  replace_observer.Wait();
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_b, root->current_url());

  // Go back to the original page.
  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  // Navigate forward twice using script.  In https://crbug.com/623319, this
  // caused a mismatch between the NavigationEntry's URL and PageState.
  EXPECT_TRUE(
      ExecJs(shell()->web_contents(), "history.forward(); history.forward();"));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_b, root->current_url());
  NavigationEntry* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(url_b, entry->GetURL());
  blink::ExplodedPageState exploded_state;
  EXPECT_TRUE(blink::DecodePageState(entry->GetPageState().ToEncodedData(),
                                     &exploded_state));
  EXPECT_EQ(url_b,
            GURL(exploded_state.top.url_string.value_or(std::u16string())));
  EXPECT_EQ(0U, exploded_state.top.children.size());

  // Go back and then forward to see if the PageState loads correctly.
  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  controller.GoForward();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  // We should be on url_b, and the renderer process shouldn't be killed.
  ASSERT_TRUE(root->current_frame_host()->IsRenderFrameLive());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_b, shell()->web_contents()->GetVisibleURL());
  EXPECT_EQ(url_b, root->current_url());
  EXPECT_EQ(0U, root->child_count());
}

// Ensure that we do not corrupt a NavigationEntry's PageState if two forward
// navigations compete in different frames, and the main frame entry contains an
// iframe of its own.  See https://crbug.com/623319.
// TODO(https://crbug.com/1101292): flaky.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    DISABLED_PageStateWithIframeAfterForwardInCompetingFrames) {
  // Navigate to a page with an iframe.
  GURL url_a(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_data_iframe.html"));
  GURL data_url("data:text/html,Subframe");
  EXPECT_TRUE(NavigateToURL(shell(), url_a));

  NavigationController& controller = shell()->web_contents()->GetController();
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_EQ(url_a, root->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->current_url());

  // Navigate the iframe to a first real page.
  GURL frame_url_a1 = embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html");
  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url_a1));

  // Navigate the iframe to a second real page.
  GURL frame_url_a2 = embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html");
  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url_a2));
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_a, root->current_url());
  EXPECT_EQ(frame_url_a2, root->child_at(0)->current_url());

  // Go back to the middle entry.
  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

  // Replace the entry with a cross-site top-level page with an iframe.  By
  // doing a replacement, the main frame pages before and after have the same
  // item sequence number, and thus going between them only requires loads in
  // the subframe.
  GURL url_b(embedded_test_server()->GetURL(
      "b.com", "/navigation_controller/page_with_data_iframe.html"));
  std::string replace_script = "location.replace('" + url_b.spec() + "')";
  TestNavigationObserver replace_observer(shell()->web_contents());
  EXPECT_TRUE(ExecJs(shell()->web_contents(), replace_script));
  replace_observer.Wait();
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_b, root->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->current_url());

  // Go back to the original page.
  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  // Navigate forward twice using script.  This will race, but in either outcome
  // we want to ensure that the subframes target entry index 1 and not 2.  In
  // https://crbug.com/623319, the subframes targeted the wrong entry, leading
  // to a URL spoof and renderer kill.
  EXPECT_TRUE(
      ExecJs(shell()->web_contents(), "history.forward(); history.forward();"));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
  EXPECT_EQ(url_b, root->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->current_url());
  NavigationEntry* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(url_b, entry->GetURL());
  blink::ExplodedPageState exploded_state;
  EXPECT_TRUE(blink::DecodePageState(entry->GetPageState().ToEncodedData(),
                                     &exploded_state));
  EXPECT_EQ(url_b,
            GURL(exploded_state.top.url_string.value_or(std::u16string())));

  // Go back and then forward to see if the PageState loads correctly.
  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  controller.GoForward();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  // We should be on url_b, and the renderer process shouldn't be killed.
  ASSERT_TRUE(root->current_frame_host()->IsRenderFrameLive());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_b, shell()->web_contents()->GetVisibleURL());
  EXPECT_EQ(url_b, root->current_url());
  EXPECT_EQ(data_url, root->child_at(0)->current_url());
}

// Ensure that forward navigations in cloned tabs can commit if they redirect to
// a different site than before.  This causes the navigation's item sequence
// number to change, meaning that we can't use it for determining whether the
// commit matches the history item.  See https://crbug.com/600238.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ForwardRedirectWithNoCommittedEntry) {
  NavigationController& controller = shell()->web_contents()->GetController();
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Put 2 pages in history.
  GURL url_1(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url_1));

  GURL url_2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url_2));

  EXPECT_EQ(url_2, root->current_url());
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

  // Do a replaceState to a URL that will redirect when we come back to it via
  // session history.
  GURL url_3(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/page_with_links.html"));
  {
    TestNavigationObserver observer(shell()->web_contents());
    std::string script =
        "history.replaceState({}, '', '/server-redirect?" + url_3.spec() + "')";
    EXPECT_TRUE(ExecJs(root, script));
    observer.Wait();
  }

  // Go back.
  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_1, root->current_url());

  // Clone the tab without navigating it.
  std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone();
  WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get());
  NavigationController& new_controller = new_tab_impl->GetController();
  FrameTreeNode* new_root = new_tab_impl->GetPrimaryFrameTree().root();
  EXPECT_TRUE(new_controller.IsInitialNavigation());
  EXPECT_TRUE(new_controller.NeedsReload());

  // Go forward in the new tab.
  {
    TestNavigationObserver observer(new_tab.get());
    new_controller.GoForward();
    observer.Wait();
  }
  EXPECT_TRUE(new_root->current_frame_host()->IsRenderFrameLive());
  EXPECT_EQ(url_3, new_root->current_url());
}

// Ensure that we can support cross-process navigations in subframes due to
// redirects.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SubframeForwardRedirect) {
  NavigationController& controller = shell()->web_contents()->GetController();
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  GURL url_1(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/page_with_data_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url_1));

  GURL frame_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), frame_url));

  EXPECT_EQ(url_1, root->current_url());
  EXPECT_EQ(frame_url, root->child_at(0)->current_url());
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());

  // Do a replaceState to a URL that will redirect cross-site when we come back
  // to it via session history.
  GURL frame_url2(embedded_test_server()->GetURL(
      "bar.com", "/navigation_controller/simple_page_2.html"));
  {
    TestNavigationObserver observer(shell()->web_contents());
    std::string script = "history.replaceState({}, '', '/server-redirect?" +
                         frame_url2.spec() + "')";
    EXPECT_TRUE(ExecJs(root->child_at(0), script));
    observer.Wait();
  }

  // Go back.
  {
    TestNavigationObserver observer(shell()->web_contents());
    controller.GoBack();
    observer.Wait();
  }
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url_1, root->current_url());

  // Go forward.
  {
    TestNavigationObserver observer(shell()->web_contents());
    controller.GoForward();
    observer.Wait();
  }
  EXPECT_TRUE(root->current_frame_host()->IsRenderFrameLive());
  EXPECT_TRUE(root->child_at(0)->current_frame_host()->IsRenderFrameLive());
  EXPECT_EQ(url_1, root->current_url());
  EXPECT_EQ(frame_url2, root->child_at(0)->current_url());
  if (AreAllSitesIsolatedForTesting()) {
    EXPECT_EQ(GURL("http://bar.com"), root->child_at(0)
                                          ->current_frame_host()
                                          ->GetSiteInstance()
                                          ->GetSiteURL());
  }
}

// Tests that when using FrameNavigationEntries, knowledge of POST navigations
// is recorded on a subframe level.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, PostInSubframe) {
  GURL page_with_form_url = embedded_test_server()->GetURL(
      "/navigation_controller/subframe_form.html");
  EXPECT_TRUE(NavigateToURL(shell(), page_with_form_url));

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  FrameTreeNode* frame = root->child_at(0);
  EXPECT_EQ(1, controller.GetEntryCount());

  {
    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    scoped_refptr<FrameNavigationEntry> root_entry = entry->GetFrameEntry(root);
    scoped_refptr<FrameNavigationEntry> frame_entry =
        entry->GetFrameEntry(frame);
    EXPECT_NE(nullptr, root_entry);
    EXPECT_NE(nullptr, frame_entry);
    EXPECT_EQ("GET", root_entry->method());
    EXPECT_EQ(-1, root_entry->post_id());
    EXPECT_EQ("GET", frame_entry->method());
    EXPECT_EQ(-1, frame_entry->post_id());
    EXPECT_FALSE(entry->GetHasPostData());
    EXPECT_EQ(-1, entry->GetPostID());
  }

  // Submit the form.
  TestNavigationObserver observer(shell()->web_contents(), 1);
  ExecuteScriptAsync(shell(), "submitForm('isubmit')");
  observer.Wait();

  EXPECT_EQ(2, controller.GetEntryCount());
  {
    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    scoped_refptr<FrameNavigationEntry> root_entry = entry->GetFrameEntry(root);
    scoped_refptr<FrameNavigationEntry> frame_entry =
        entry->GetFrameEntry(frame);
    EXPECT_NE(nullptr, root_entry);
    EXPECT_NE(nullptr, frame_entry);
    EXPECT_EQ("GET", root_entry->method());
    EXPECT_EQ(-1, root_entry->post_id());
    EXPECT_EQ("POST", frame_entry->method());
    EXPECT_NE(-1, frame_entry->post_id());
    EXPECT_FALSE(entry->GetHasPostData());
    EXPECT_EQ(-1, entry->GetPostID());
  }
}

// Tests that POST body is not lost when decidePolicyForNavigation tells the
// renderer to route the request via OpenURL mojo method sent to the browser.
// See also https://crbug.com/344348.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, PostViaOpenUrlMsg) {
  GURL main_url(
      embedded_test_server()->GetURL("/form_that_posts_to_echoall.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  // Ask the renderer to go through OpenURL Mojo method. Without this, the test
  // wouldn't repro https://crbug.com/344348.
  shell()
      ->web_contents()
      ->GetMutableRendererPrefs()
      ->browser_handles_all_top_level_requests = true;
  shell()->web_contents()->SyncRendererPrefs();

  // Submit the form.
  TestNavigationObserver form_post_observer(shell()->web_contents(), 1);
  EXPECT_TRUE(ExecJs(shell()->web_contents(),
                     "document.getElementById('form').submit();"));
  form_post_observer.Wait();

  // Verify that we arrived at the expected location.
  GURL target_url(embedded_test_server()->GetURL("/echoall"));
  EXPECT_EQ(target_url, shell()->web_contents()->GetLastCommittedURL());

  // Verify that POST body was correctly passed to the server and ended up in
  // the body of the page.
  EXPECT_EQ(
      "text=value\n",
      EvalJs(shell(), "document.getElementsByTagName('pre')[0].innerText"));
}

// This test verifies that reloading a POST request that is uncacheable won't
// incorrectly result in a GET request.  This is a regression test for
// https://crbug.com/860807.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, UncacheablePost) {
  GURL main_url(embedded_test_server()->GetURL(
      "initial-page.example.com", "/form_that_posts_to_echoall_nocache.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  WebContents* web_contents = shell()->web_contents();
  EXPECT_EQ(0, web_contents->GetController().GetLastCommittedEntryIndex());

  // Tweak the test page, so that it POSTs directly to the right cross-site URL
  // (without going through the /cross-site-307/host.com handler, because it
  // seems that such redirects do not preserve the Origin header).
  GURL target_url(
      embedded_test_server()->GetURL("another-site.com", "/echoall/nocache"));
  ASSERT_TRUE(ExecJs(
      web_contents,
      JsReplace("document.getElementById('form').action = $1", target_url)));

  // Submit the form.
  TestNavigationObserver form_post_observer(web_contents, 1);
  EXPECT_TRUE(
      ExecJs(web_contents, "document.getElementById('form').submit();"));
  form_post_observer.Wait();

  // Verify that we arrived at the expected location.
  EXPECT_EQ(target_url, web_contents->GetLastCommittedURL());
  EXPECT_EQ(1, web_contents->GetController().GetLastCommittedEntryIndex());

  // Verify that this was a POST request.
  std::string request_headers =
      EvalJs(web_contents, "document.getElementsByTagName('pre')[1].innerText;")
          .ExtractString();
  EXPECT_THAT(request_headers, ::testing::HasSubstr("POST /echoall/nocache"));

  // Verify that POST body was correctly passed to the server and ended up in
  // the body of the page.
  EXPECT_EQ("text=value\n",
            EvalJs(web_contents,
                   "document.getElementsByTagName('pre')[0].innerText;"));

  // Extract the response nonce.
  std::string old_response_nonce =
      EvalJs(web_contents,
             "document.getElementById('response-nonce').innerText")
          .ExtractString();

  // Verify that the Origin header correctly reflects the initial initiator.
  EXPECT_THAT(EvalJs(web_contents,
                     "document.getElementById('request-headers').innerText")
                  .ExtractString(),
              ::testing::HasSubstr("Origin: http://initial-page.example.com"));

  // Go back.
  {
    TestNavigationObserver observer(web_contents);
    web_contents->GetController().GoBack();
    observer.Wait();
  }
  EXPECT_EQ(main_url, web_contents->GetLastCommittedURL());
  EXPECT_EQ(0, web_contents->GetController().GetLastCommittedEntryIndex());

  // Go forward.
  {
    TestNavigationObserver navigation_observer(web_contents);
    NavigationHandleObserver handle_observer(web_contents, target_url);
    web_contents->GetController().GoForward();
    navigation_observer.Wait();

    // Verify that the previous response response really was treated as
    // uncacheable.
    EXPECT_TRUE(handle_observer.is_error());
    EXPECT_EQ(net::ERR_CACHE_MISS, handle_observer.net_error_code());
  }
  EXPECT_EQ(target_url, web_contents->GetLastCommittedURL());
  EXPECT_EQ(1, web_contents->GetController().GetLastCommittedEntryIndex());

  // Reload
  {
    TestNavigationObserver observer(web_contents);
    web_contents->GetController().Reload(content::ReloadType::NORMAL,
                                         false);  // check_for_repost
    observer.Wait();
  }
  EXPECT_EQ(target_url, web_contents->GetLastCommittedURL());
  EXPECT_EQ(1, web_contents->GetController().GetLastCommittedEntryIndex());

  // MAIN VERIFICATION for https://crbug.com/860807: Verify that the reload was
  // a POST request.
  request_headers =
      EvalJs(web_contents, "document.getElementsByTagName('pre')[1].innerText;")
          .ExtractString();
  EXPECT_THAT(request_headers, ::testing::HasSubstr("POST /echoall/nocache"));

  // Verify that POST body was correctly passed to the server and ended up in
  // the body of the page.  This is supplementary verification against
  // https://crbug.com/860807.
  EXPECT_EQ("text=value\n",
            EvalJs(web_contents,
                   "document.getElementsByTagName('pre')[0].innerText;"));

  // Extract the new response nonce and verify that it did change (e.g. that the
  // reload did load fresh content).
  EXPECT_NE(old_response_nonce,
            EvalJs(web_contents,
                   "document.getElementById('response-nonce').innerText"));

  // Verify that the Origin header correctly reflects the initial initiator.
  // This is a regression test for https://crbug.com/915538.
  EXPECT_THAT(EvalJs(web_contents,
                     "document.getElementById('request-headers').innerText")
                  .ExtractString(),
              ::testing::HasSubstr("Origin: http://initial-page.example.com"));
}

// This test verifies that it is possible to reload a POST request that
// initially failed (e.g. because the network was offline or the host was
// unreachable during the initial navigation).  This is a regression test for
// https://crbug.com/869117.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ReloadOfInitiallyFailedPost) {
  GURL main_url(embedded_test_server()->GetURL(
      "/form_that_posts_to_echoall_nocache.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  WebContents* web_contents = shell()->web_contents();
  EXPECT_EQ(0, web_contents->GetController().GetLastCommittedEntryIndex());

  // Submit the form while simulating "network down" conditions.
  GURL target_url(embedded_test_server()->GetURL("/echoall/nocache"));
  {
    std::unique_ptr<URLLoaderInterceptor> interceptor =
        URLLoaderInterceptor::SetupRequestFailForURL(
            target_url, net::ERR_INTERNET_DISCONNECTED);
    TestNavigationObserver form_post_observer(web_contents, 1);
    EXPECT_TRUE(
        ExecJs(web_contents, "document.getElementById('form').submit();"));
    form_post_observer.Wait();
  }
  EXPECT_EQ(target_url, web_contents->GetLastCommittedURL());
  EXPECT_EQ(1, web_contents->GetController().GetLastCommittedEntryIndex());

  // Reload
  {
    TestNavigationObserver observer(web_contents);
    web_contents->GetController().Reload(content::ReloadType::NORMAL,
                                         false);  // check_for_repost
    observer.Wait();
  }
  EXPECT_EQ(target_url, web_contents->GetLastCommittedURL());
  EXPECT_EQ(1, web_contents->GetController().GetLastCommittedEntryIndex());

  // Verify that the reload was a POST request.
  std::string request_headers =
      EvalJs(web_contents, "document.getElementsByTagName('pre')[1].innerText;")
          .ExtractString();
  EXPECT_THAT(request_headers, ::testing::HasSubstr("POST /echoall/nocache"));

  // Verify that POST body was correctly passed to the server and ended up in
  // the body of the page.
  EXPECT_EQ("text=value\n",
            EvalJs(web_contents,
                   "document.getElementsByTagName('pre')[0].innerText;"));
}

// Tests that inserting a named subframe into the FrameTree clears any
// previously existing FrameNavigationEntry objects for the same name.
// See https://crbug.com/628677.
// Crashes/fails inconsistently on windows and ChromeOS:
// https://crbug.com/783806.
// Flaky on every platforms:
// https://crbug.com/765107#c15
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       DISABLED_EnsureFrameNavigationEntriesClearedOnMismatch) {
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  NavigationControllerImpl& controller = web_contents->GetController();
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();

  // Start by navigating to a page with complex frame hierarchy.
  GURL start_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  EXPECT_EQ(3U, root->child_count());
  EXPECT_EQ(2U, root->child_at(0)->child_count());

  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();

  // Verify only the parts of the NavigationEntry affected by this test.
  {
    // * Main frame has 3 subframes.
    scoped_refptr<FrameNavigationEntry> root_entry = entry->GetFrameEntry(root);
    EXPECT_NE(nullptr, root_entry);
    EXPECT_EQ("", root_entry->frame_unique_name());
    EXPECT_EQ(3U, entry->root_node()->children.size());
    EXPECT_EQ(2U, entry->root_node()->children[0]->children.size());

    // * The first child of the main frame is named and has two more children.
    FrameTreeNode* frame = root->child_at(0)->child_at(0);
    NavigationEntryImpl::TreeNode* tree_node = entry->GetTreeNode(frame);
    scoped_refptr<FrameNavigationEntry> frame_entry =
        entry->GetFrameEntry(frame);
    EXPECT_NE(nullptr, tree_node);
    EXPECT_NE(nullptr, frame_entry);
    EXPECT_EQ("1-1: 2-1: name", frame_entry->frame_unique_name());
    EXPECT_EQ(frame_entry, tree_node->frame_entry);
    EXPECT_EQ(0U, tree_node->children.size());
  }

  // Removing the first child of the main frame should remove the corresponding
  // FrameTreeNode.
  EXPECT_TRUE(ExecJs(root, kRemoveFrameScript));
  EXPECT_EQ(2U, root->child_count());

  // However, the FrameNavigationEntry objects for the frame that was removed
  // should still be around.
  {
    scoped_refptr<FrameNavigationEntry> root_entry = entry->GetFrameEntry(root);
    EXPECT_NE(nullptr, root_entry);
    EXPECT_EQ(3U, entry->root_node()->children.size());
    EXPECT_EQ(2U, entry->root_node()->children[0]->children.size());

    // Since child count is known only to the FrameNavigationEntry::TreeNode,
    // traverse the root entry to find the correct one matching the
    // frame_unique_name. The ordering of entries in the FrameNavigationEntry
    // tree is not guaranteed to be the same as the order in the FrameTreeNode
    // tree. The latter depends on the order of frames committing navigations,
    // which is undefined and depends on responses from the network.
    // Traverse the FrameNavigationEntry tree, since the FrameTreeNode has
    // been deleted and cannot be used for looking up the TreeNode.
    NavigationEntryImpl::TreeNode* tree_node = nullptr;
    for (auto& node : entry->root_node()->children[0]->children) {
      if (node->frame_entry->frame_unique_name() == "1-1: 2-1: name") {
        tree_node = node.get();
        break;
      }
    }
    EXPECT_TRUE(tree_node);
    EXPECT_EQ(0U, tree_node->children.size());
  }

  // Now, insert a frame with the same name as the previously removed one
  // at a different layer of the frame tree.
  FrameTreeNode* subframe = root->child_at(1)->child_at(1)->child_at(0);
  EXPECT_EQ(2U, root->child_at(1)->child_count());
  EXPECT_EQ(0U, subframe->child_count());
  std::string add_matching_name_frame_script =
      "var f = document.createElement('iframe');"
      "f.name = '1-1-name';"
      "f.src = '1-1.html';"
      "document.body.appendChild(f);";
  TestNavigationObserver observer(web_contents, 1);
  EXPECT_TRUE(ExecJs(subframe, add_matching_name_frame_script));
  EXPECT_EQ(1U, subframe->child_count());
  observer.Wait();

  // Verify that the FrameNavigationEntry for the original frame is now gone.
  {
    scoped_refptr<FrameNavigationEntry> root_entry = entry->GetFrameEntry(root);
    EXPECT_NE(nullptr, root_entry);
    EXPECT_EQ(3U, entry->root_node()->children.size());

    // Both children of |entry->root_node()->children[0]| should be removed by
    // NavigationEntryImpl::RemoveEntryForFrame, because both will have
    // colliding unique names (the removed parent and the newly added frame both
    // load '1-1.html' - which has 2 named framse).
    EXPECT_EQ(0U, entry->root_node()->children[0]->children.size());
  }
}

// This test ensures that the comparison of tree position between a
// FrameTreeNode and FrameNavigationEntry works correctly for matching
// first-level frames.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       EnsureFirstLevelFrameNavigationEntriesMatch) {
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();

  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  NavigationEntryImpl* nav_entry =
      web_contents->GetController().GetLastCommittedEntry();

  // Add, then remove a named frame. It will create a FrameNavigationEntry
  // for the name and remove it (since this is a frame created by script).
  EXPECT_TRUE(ExecJs(root, kAddNamedFrameScript));
  EXPECT_EQ(1U, root->child_count());
  EXPECT_EQ(1U, nav_entry->root_node()->children.size());
  scoped_refptr<FrameNavigationEntry> old_fne =
      nav_entry->root_node()->children[0]->frame_entry;

  EXPECT_TRUE(ExecJs(root, kRemoveFrameScript));
  EXPECT_EQ(0U, root->child_count());
  EXPECT_EQ(0U, nav_entry->root_node()->children.size());

  // Add another frame with the same name as before. The matching logic should
  // NOT consider them the same and should NOT result in the
  // FrameNavigationEntry being reused (because the frame injected by javascript
  // will get a fresh, random unique name each time it is created or recreated).
  EXPECT_TRUE(ExecJs(root, kAddNamedFrameScript));
  EXPECT_EQ(1U, root->child_count());
  EXPECT_EQ(1U, nav_entry->root_node()->children.size());
  scoped_refptr<FrameNavigationEntry> new_fne =
      nav_entry->root_node()->children[0]->frame_entry;
  EXPECT_TRUE(old_fne->HasOneRef());  // Only the test keeps the old FNE alive.
  EXPECT_NE(old_fne.get(), new_fne.get());

  EXPECT_TRUE(ExecJs(root, kRemoveFrameScript));
  EXPECT_EQ(0U, root->child_count());
}

// Test that converted reload navigations classified as EXISTING_ENTRY properly
// update all the members of FrameNavigationEntry if they redirect. If not, it
// is possible to get a mismatch between the origin and URL of a document as
// seen in https://crbug.com/630103.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       EnsureSameURLNavigationUpdatesFrameNavigationEntry) {
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();

  // Navigate to a simple page and then perform a fragment change navigation.
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  GURL fragment_change_url(
      embedded_test_server()->GetURL("a.com", "/title1.html#foo"));
  EXPECT_TRUE(NavigateToURL(shell(), fragment_change_url));
  EXPECT_EQ(2, web_contents->GetController().GetEntryCount());

  // Replace the URL of the current NavigationEntry with one that will cause
  // a server redirect when loaded.
  {
    GURL redirect_dest_url(
        embedded_test_server()->GetURL("sub.a.com", "/simple_page.html"));
    TestNavigationObserver observer(web_contents);
    std::string script = "history.replaceState({}, '', '/server-redirect?" +
                         redirect_dest_url.spec() + "')";
    EXPECT_TRUE(ExecJs(root, script));
    observer.Wait();
  }

  // Simulate the user hitting Enter in the omnibox without changing the URL.
  {
    TestNavigationObserver observer(web_contents);
    web_contents->GetController().LoadURL(web_contents->GetLastCommittedURL(),
                                          Referrer(), ui::PAGE_TRANSITION_LINK,
                                          std::string());
    observer.Wait();
  }

  // Prior to fixing the issue, the above omnibox navigation (which is now
  // classified as EXISTING_ENTRY) was leaving the FrameNavigationEntry with the
  // same document sequence number as the previous entry but updates the URL.
  // Doing a back session history navigation now will cause the browser to
  // consider it as same document because of this matching document sequence
  // number and lead to a mismatch of origin and URL in the renderer process.
  {
    TestNavigationObserver observer(web_contents);
    web_contents->GetController().GoBack();
    observer.Wait();
  }

  // Verify the expected origin through JavaScript. It also has the additional
  // verification of the process also being still alive.
  EXPECT_EQ(url::Origin::Create(start_url).Serialize(),
            EvalJs(web_contents, "self.origin"));
}

// Helper to trigger a history-back navigation in the WebContents after the
// renderer has committed a same-process and cross-origin navigation to the
// given |url|, but before the browser side has had a chance to process the
// DidCommitProvisionalLoad message.
class HistoryNavigationBeforeCommitInjector
    : public DidCommitNavigationInterceptor {
 public:
  HistoryNavigationBeforeCommitInjector(WebContentsImpl* web_contents,
                                        const GURL& url)
      : DidCommitNavigationInterceptor(web_contents),
        did_trigger_history_navigation_(false),
        url_(url) {}

  HistoryNavigationBeforeCommitInjector(
      const HistoryNavigationBeforeCommitInjector&) = delete;
  HistoryNavigationBeforeCommitInjector& operator=(
      const HistoryNavigationBeforeCommitInjector&) = delete;

  ~HistoryNavigationBeforeCommitInjector() override {}

  bool did_trigger_history_navigation() const {
    return did_trigger_history_navigation_;
  }

 private:
  // DidCommitNavigationInterceptor:
  bool WillProcessDidCommitNavigation(
      RenderFrameHost* render_frame_host,
      NavigationRequest* navigation_request,
      mojom::DidCommitProvisionalLoadParamsPtr* params,
      mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
      override {
    if (!render_frame_host->GetParent() && (**params).url == url_) {
      did_trigger_history_navigation_ = true;
      web_contents()->GetController().GoBack();
    }
    return true;
  }

  bool did_trigger_history_navigation_;
  GURL url_;
};

// Test which simulates a race condition between a cross-origin, same-process
// navigation and a same document session history navigation. When such a race
// occurs, the renderer will commit the cross-origin navigation, updating its
// version of the current document sequence number, and will send an IPC to the
// browser process. The session history navigation comes after the commit for
// the cross-origin navigation and updates the URL, but not the origin of the
// document. This results in mismatch between the two and causes the renderer
// process to be killed. See https://crbug.com/630103.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    RaceCrossOriginNavigationAndSameDocumentHistoryNavigation) {
  if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
    // When RenderDocument is enabled, the cross-document navigation uses a
    // new RenderFrameHost to commit the navigation, causing the previous
    // RenderFrameHost to be deleted and the same-document navigation to be
    // cancelled, so just return early.
    // TODO(https://crbug.com/936696): When
    // ShouldAvoidRedundantNavigationCancellations() returns true, we won't
    // actually cancel the same-document navigation as it still lives in the
    // FrameTreeNode (instead of owned by the swapped out RenderFrameHost), but
    // apparently there is a bug with same-document history navigations that
    // happen while there are pending cross-document commits, where we will
    // still try to trigger beforeunload, causing the same-document history
    // navigation to get stalled forever. Also, due to the way the history
    // navigation injection works, it might trigger the deletion of the pending
    // commit RenderFrameHost while in the middle of processing the
    // DidCommitNavigation call because the history navigation will try to
    // create a new speculative RenderFrameHost, unless when navigation queueing
    // is enabled. After fixing the beforeunload bug, we should be able to
    // continue running the test when RenderDocument is enabled
    // and ShouldQueueNavigationsWhenPendingCommitRFHExists() is true.
    return;
  }
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();

  // Navigate to a simple page and then perform a same document navigation.
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  GURL same_document_url(
      embedded_test_server()->GetURL("a.com", "/title1.html#foo"));
  EXPECT_TRUE(NavigateToURL(shell(), same_document_url));
  EXPECT_EQ(2, web_contents->GetController().GetEntryCount());

  // Create a HistoryNavigationBeforeCommitInjector, which will perform a
  // GoBack() just before a cross-origin, same process navigation commits.
  GURL cross_origin_url(
      embedded_test_server()->GetURL("suborigin.a.com", "/title2.html"));
  HistoryNavigationBeforeCommitInjector trigger(web_contents, cross_origin_url);

  // Navigate cross-origin, waiting for the commit to occur.
  UrlCommitObserver cross_origin_commit_observer(root, cross_origin_url);
  UrlCommitObserver history_commit_observer(root, start_url);
  shell()->LoadURL(cross_origin_url);
  cross_origin_commit_observer.Wait();
  EXPECT_EQ(cross_origin_url, web_contents->GetLastCommittedURL());
  EXPECT_EQ(2, web_contents->GetController().GetLastCommittedEntryIndex());
  EXPECT_TRUE(trigger.did_trigger_history_navigation());

  // Wait for the back navigation to commit as well.
  history_commit_observer.Wait();
  EXPECT_EQ(start_url, web_contents->GetLastCommittedURL());
  EXPECT_EQ(0, web_contents->GetController().GetLastCommittedEntryIndex());
  EXPECT_EQ(3, web_contents->GetController().GetEntryCount());

  // Verify the expected origin through JavaScript. It also has the additional
  // verification of the process also being still alive.
  EXPECT_EQ(url::Origin::Create(start_url).Serialize(),
            EvalJs(web_contents, "self.origin"));
}

// This test simulates what happens when OnCommitTimeout is triggered after
// ResetForCrossDocumentRestart. See https://crbug.com/1006677.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       OnCommitTimeoutAfterResetForCrossDocumentRestart) {
  if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
    // When RenderDocument is enabled, the cross-document navigation uses a
    // new RenderFrameHost to commit the navigation, causing the previous
    // RenderFrameHost to be deleted and the same-document navigation to be
    // cancelled, so just return early.
    // TODO(https://crbug.com/936696): When
    // ShouldAvoidRedundantNavigationCancellations() returns true, we won't
    // actually cancel the same-document navigation as it still lives in the
    // FrameTreeNode (instead of owned by the swapped out RenderFrameHost), but
    // apparently there is a bug with same-document history navigations that
    // happen while there are pending cross-document commits, where we will
    // still try to trigger beforeunload, causing the same-document history
    // navigation to get stalled forever. Also, due to the way the history
    // navigation injection works, it might trigger the deletion of the pending
    // commit RenderFrameHost while in the middle of processing the
    // DidCommitNavigation call because the history navigation will try to
    // create a new speculative RenderFrameHost, unless when navigation queueing
    // is enabled. After fixing the beforeunload bug, we should be able to
    // continue running the test when RenderDocument is enabled
    // and ShouldQueueNavigationsWhenPendingCommitRFHExists() is true.
    return;
  }

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();

  // Navigate to a simple page and then perform a same document navigation.
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  GURL same_document_url(
      embedded_test_server()->GetURL("a.com", "/title1.html#foo"));
  EXPECT_TRUE(NavigateToURL(shell(), same_document_url));
  EXPECT_EQ(2, web_contents->GetController().GetEntryCount());

  // Create a HistoryNavigationBeforeCommitInjector, which will perform a
  // GoBack() just before a cross-origin, same process navigation commits.
  GURL cross_origin_url(
      embedded_test_server()->GetURL("suborigin.a.com", "/title2.html"));
  HistoryNavigationBeforeCommitInjector trigger(web_contents, cross_origin_url);

  // Trigger OnCommitTimeout by setting commit timeout to 1 microsecond.
  NavigationRequest::SetCommitTimeoutForTesting(base::Microseconds(1));

  // Navigate cross-origin, waiting for the commit to occur.
  UrlCommitObserver cross_origin_commit_observer(root, cross_origin_url);
  UrlCommitObserver history_commit_observer(root, start_url);
  shell()->LoadURL(cross_origin_url);
  cross_origin_commit_observer.Wait();
  EXPECT_EQ(cross_origin_url, web_contents->GetLastCommittedURL());
  EXPECT_EQ(2, web_contents->GetController().GetLastCommittedEntryIndex());

  // Wait for the history navigation to commit.
  history_commit_observer.Wait();
  EXPECT_EQ(start_url, web_contents->GetLastCommittedURL());
  EXPECT_EQ(0, web_contents->GetController().GetLastCommittedEntryIndex());

  // Reset the timeout.
  NavigationRequest::SetCommitTimeoutForTesting(base::TimeDelta());
}

// This test simulates a same-document navigation racing with a cross-document
// one. Historically this would have been started as a same-document navigation
// then restarted by the renderer as a cross-document navigation (see
// https://crbug.com/936962).
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTestNoServer,
                       SameDocumentNavigationRaceWithCrossDocumentNavigation) {
  net::test_server::ControllableHttpResponse response_success(
      embedded_test_server(), "/title1.html");
  ASSERT_TRUE(embedded_test_server()->Start());

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();

  // 1. Navigate to a simple page with no-cache, no-store.
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  {
    UrlCommitObserver history_commit_observer(root, start_url);
    shell()->LoadURL(start_url);
    response_success.WaitForRequest();
    response_success.Send(
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/html; charset=utf-8\r\n"
        "Cache-Control: no-cache, no-store\r\n"
        "\r\n"
        "The server speaks HTTP!");
    response_success.Done();
    history_commit_observer.Wait();
    EXPECT_EQ(0, web_contents->GetController().GetLastCommittedEntryIndex());
  }

  // 2. Perform a same-document navigation forward.
  {
    GURL same_document_url(
        embedded_test_server()->GetURL("a.com", "/title1.html#foo"));
    EXPECT_TRUE(NavigateToURL(shell(), same_document_url));
    EXPECT_EQ(1, web_contents->GetController().GetLastCommittedEntryIndex());
  }

  // 3. Create a HistoryNavigationBeforeCommitInjector, which will perform a
  // same-document back navigation just before a cross-document navigation
  // commits. This triggers a race condition and forces the
  // same-document navigation to become a cross-document navigation.
  {
    NavigationHandleCommitObserver back_navigation(web_contents, start_url);

    GURL cross_document_url(
        embedded_test_server()->GetURL("a.com", "/title2.html"));
    HistoryNavigationBeforeCommitInjector trigger(web_contents,
                                                  cross_document_url);

    // Navigate cross-document, waiting for the commit to occur.
    UrlCommitObserver cross_doc_commit_observer(root, cross_document_url);
    shell()->LoadURL(cross_document_url);
    EXPECT_TRUE(web_contents->GetController().GetPendingEntry());
    cross_doc_commit_observer.Wait();

    // The cross-document navigation is done, and we're at history entry 2 (the
    // third document in the list).
    EXPECT_EQ(cross_document_url, web_contents->GetLastCommittedURL());
    EXPECT_EQ(2, web_contents->GetController().GetLastCommittedEntryIndex());
    // Verify the same-document history navigation was started before this
    // committed.
    EXPECT_TRUE(trigger.did_trigger_history_navigation());

    if (ShouldCreateNewHostForAllFrames()) {
      // When RenderDocument is enabled, the cross-document navigation used a
      // new RenderFrameHost to commit the navigation, causing the previous
      // RenderFrameHost to be deleted and the same-document navigation to be
      // cancelled. Since there are no navigations left, just return early.
      // TODO(https://crbug.com/936696): When
      // ShouldAvoidRedundantNavigationCancellations() returns true, we won't
      // actually cancel the same-document navigation as it still lives in the
      // FrameTreeNode (instead of owned by the swapped out RenderFrameHost),
      // but apparently there is a bug with same-document history navigations
      // that happen while there are pending cross-document commits, where we
      // will still try to trigger beforeunload, causing the same-document
      // history navigation to get stalled forever. After fixing that bug, we
      // should be able to continue running the test when RenderDocument is
      // enabled and ShouldAvoidRedundantNavigationCancellations() is true.
      EXPECT_EQ(ShouldAvoidRedundantNavigationCancellations(),
                !!root->navigation_request());
      return;
    }

    // Otherwise, the same-document back navigation had to be converted to a
    // cross-document navigation because it was racing with, and will complete
    // after, the cross-document navigation. It is still waiting to complete.
    EXPECT_TRUE(root->navigation_request());
    // This is the history navigation.
    EXPECT_EQ(root->navigation_request()->common_params().url.spec(),
              start_url.spec());
    // It was not same-document because of the race.
    EXPECT_FALSE(root->navigation_request()->IsSameDocument());

    UrlCommitObserver back_history_commit_observer(root, start_url);
    back_history_commit_observer.Wait();
    // The back navigation completes afterward. There is no more requests to
    // run, and no pending commits left.
    EXPECT_FALSE(root->navigation_request());
    EXPECT_FALSE(root->current_frame_host()->HasPendingCommitNavigation());
    // The back navigation was not same-document due to the race with a
    // cross-document navigation committing first.
    EXPECT_TRUE(back_navigation.has_committed());
    EXPECT_FALSE(back_navigation.was_same_document());
    // The back navigation took us back to the expected history entry.
    EXPECT_EQ(0, web_contents->GetController().GetLastCommittedEntryIndex());
  }
}

// Test that verifies that Referer and Origin http headers are correctly sent
// to the final destination of a cross-site POST with a few redirects thrown in.
// This test is somewhat related to https://crbug.com/635400.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RefererAndOriginHeadersAfterRedirects) {
  // Navigate to the page with form that posts via 307 redirection to
  // |redirect_target_url| (cross-site from |form_url|).  Using 307 (rather than
  // 302) redirection is important to preserve the HTTP method and POST body.
  GURL form_url(embedded_test_server()->GetURL(
      "a.com", "/form_that_posts_cross_site.html"));
  GURL redirect_target_url(embedded_test_server()->GetURL("x.com", "/echoall"));
  EXPECT_TRUE(NavigateToURL(shell(), form_url));

  // Submit the form.  The page submitting the form is at 0, and will
  // go through 307 redirects from 1 -> 2 and 2 -> 3:
  // 0. http://a.com:.../form_that_posts_cross_site.html
  // 1. http://a.com:.../cross-site-307/i.com/cross-site-307/x.com/echoall
  // 2. http://i.com:.../cross-site-307/x.com/echoall
  // 3. http://x.com:.../echoall/
  TestNavigationObserver form_post_observer(shell()->web_contents(), 1);
  EXPECT_TRUE(
      ExecJs(shell(), "document.getElementById('text-form').submit();"));
  form_post_observer.Wait();

  // Verify that we arrived at the expected, redirected location.
  EXPECT_EQ(redirect_target_url,
            shell()->web_contents()->GetLastCommittedURL());

  // Get the http request headers.
  std::string headers =
      EvalJs(shell(), "document.getElementsByTagName('pre')[1].innerText")
          .ExtractString();

  // Verify the Origin and Referer headers.
  EXPECT_THAT(headers, ::testing::HasSubstr("Origin: null"));
  EXPECT_THAT(headers, ::testing::ContainsRegex("Referer: http://a.com:.*/"));
}

// Test that verifies that Content-Type http header is correctly sent
// to the final destination of a cross-site POST with a few redirects thrown in.
// Test for https://crbug.com/860546.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ContentTypeHeaderAfterRedirectAndRefresh) {
  // Navigate to the page with form that posts via 307 redirection to
  // |redirect_target_url| (cross-site from |form_url|).  Using 307 (rather than
  // 302) redirection is important to preserve the HTTP method and POST body.
  GURL form_url(embedded_test_server()->GetURL(
      "a.com", "/form_that_posts_cross_site.html"));
  GURL redirect_target_url(embedded_test_server()->GetURL("x.com", "/echoall"));
  EXPECT_TRUE(NavigateToURL(shell(), form_url));

  // Submit the form.  The page submitting the form is at 0, and will
  // go through 307 redirects from 1 -> 2 and 2 -> 3:
  // 0. http://a.com:.../form_that_posts_cross_site.html
  // 1. http://a.com:.../cross-site-307/i.com/cross-site-307/x.com/echoall
  // 2. http://i.com:.../cross-site-307/x.com/echoall
  // 3. http://x.com:.../echoall/
  TestNavigationObserver form_post_observer(shell()->web_contents(), 1);
  EXPECT_TRUE(
      ExecJs(shell(), "document.getElementById('text-form').submit();"));
  form_post_observer.Wait();

  // Verify that we arrived at the expected, redirected location.
  EXPECT_EQ(redirect_target_url,
            shell()->web_contents()->GetLastCommittedURL());

  // Get the http request headers.
  std::string headers =
      EvalJs(shell(), "document.getElementsByTagName('pre')[1].innerText")
          .ExtractString();

  // Verify the Content-Type header.
  EXPECT_THAT(headers, ::testing::HasSubstr(
                           "Content-Type: application/x-www-form-urlencoded"));

  // Reload the page.
  TestNavigationObserver reload_observer(shell()->web_contents(), 1);
  ASSERT_TRUE(ExecJs(shell(), "location.reload()"));
  reload_observer.Wait();

  // Re-verify the Content-Type header.
  headers = EvalJs(shell(), "document.getElementsByTagName('pre')[1].innerText")
                .ExtractString();
  EXPECT_THAT(headers, ::testing::HasSubstr(
                           "Content-Type: application/x-www-form-urlencoded"));
}

// Check that the favicon is not cleared for same document navigations.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SameDocumentNavigationDoesNotClearFavicon) {
  // Load a page and fake a favicon for it.
  NavigationController& controller = shell()->web_contents()->GetController();
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("/simple_page.html")));
  NavigationEntry* entry = controller.GetLastCommittedEntry();
  ASSERT_TRUE(entry);
  content::FaviconStatus& favicon_status = entry->GetFavicon();
  favicon_status.valid = true;

  ASSERT_TRUE(RendererLocationReplace(
      shell(), embedded_test_server()->GetURL(
                   "/simple_page.html#same-document-navigation")));
  entry = controller.GetLastCommittedEntry();
  content::FaviconStatus& favicon_status2 = entry->GetFavicon();
  EXPECT_TRUE(favicon_status2.valid);

  ASSERT_TRUE(RendererLocationReplace(
      shell(),
      embedded_test_server()->GetURL("/simple_page.html?new-navigation")));
  entry = controller.GetLastCommittedEntry();
  content::FaviconStatus& favicon_status3 = entry->GetFavicon();
  EXPECT_FALSE(favicon_status3.valid);
}

namespace {

class AllowDialogInterceptor
    : public blink::mojom::LocalFrameHostInterceptorForTesting {
 public:
  AllowDialogInterceptor() = default;
  ~AllowDialogInterceptor() override = default;

  void Init(RenderFrameHostImpl* render_frame_host) {
    render_frame_host_ = render_frame_host;
    std::ignore = render_frame_host_->local_frame_host_receiver_for_testing()
                      .SwapImplForTesting(this);
  }

  blink::mojom::LocalFrameHost* GetForwardingInterface() override {
    return render_frame_host_;
  }

  void RunModalAlertDialog(const std::u16string& alert_message,
                           bool disable_third_party_subframe_suppresion,
                           RunModalAlertDialogCallback callback) override {
    alert_callback_ = std::move(callback);
    alert_message_ = alert_message;
  }

  void ResumeProcessingModalAlertDialogHandling() {
    has_called_callback_ = true;
    render_frame_host_->RunModalAlertDialog(alert_message_, false,
                                            std::move(alert_callback_));
  }

  bool HasCalledAlertCallback() const { return has_called_callback_; }

 private:
  raw_ptr<RenderFrameHostImpl> render_frame_host_;
  std::u16string alert_message_;
  RunModalAlertDialogCallback alert_callback_;
  bool has_called_callback_ = false;
};

class NavigationControllerAlertDialogBrowserTest
    : public NavigationControllerBrowserTest,
      public WebContentsObserver,
      public WebContentsDelegate {
 public:
  void BindWebContents(WebContents* web_contents) {
    alert_interceptor_.Init(
        static_cast<RenderFrameHostImpl*>(web_contents->GetPrimaryMainFrame()));
    Observe(web_contents);
    web_contents->SetDelegate(this);
  }

  void DidFinishNavigation(NavigationHandle* navigation_handle) override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    if (!navigation_handle->HasCommitted())
      return;

    // Continue handling the rest of the alert dialog handling.
    alert_interceptor_.ResumeProcessingModalAlertDialogHandling();
  }

  // WebContentsDelegate:
  JavaScriptDialogManager* GetJavaScriptDialogManager(
      WebContents* source) override {
    CHECK(false);
    return nullptr;  // agh compiler
  }

  bool HasCalledAlertCallback() const {
    return alert_interceptor_.HasCalledAlertCallback();
  }

 private:
  AllowDialogInterceptor alert_interceptor_;
};

}  // namespace

// Check that swapped out frames cannot spawn JavaScript dialogs.
// TODO(crbug.com/1112336): Flaky
IN_PROC_BROWSER_TEST_P(NavigationControllerAlertDialogBrowserTest,
                       DISABLED_NoDialogsFromSwappedOutFrames) {
  // Start on a normal page.
  GURL url1 = embedded_test_server()->GetURL(
      "/navigation_controller/beforeunload_dialog.html");
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  // Bind the WebContents observer to start watching for the finished
  // navigation callback. When the navigation is called we resume the
  // suspended alert dialog handling.
  WebContents* web_contents = shell()->web_contents();
  BindWebContents(web_contents);

  // Use a chrome:// url to force the second page to be in a different process.
  GURL url2(std::string(kChromeUIScheme) + url::kStandardSchemeSeparator +
            kChromeUIGpuHost);
  EXPECT_TRUE(NavigateToURL(shell(), url2));

  // What happens now is that attempting to unload the first page will trigger a
  // JavaScript alert but allow navigation. The alert mojo message will be
  // suspended by the subclassed RenderFrameHostImplForAllowDialogInterceptor.
  // The commit of the second page will cause the alert dialog message handling
  // to resume. If the dialog mojo message is allowed to spawn a dialog, the
  // call by the WebContents to its delegate to get the JavaScriptDialogManager
  // will cause a CHECK and the test will fail.
  EXPECT_TRUE(HasCalledAlertCallback());
}

// Check that the referrer is stored inside FrameNavigationEntry for subframes.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RefererStoredForSubFrame) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  GURL url_simple(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe_simple.html"));
  GURL url_redirect(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe_redirect.html"));

  // Run this test twice: with and without a redirection.
  for (const GURL& url : {url_simple, url_redirect}) {
    // Navigate to a page with an iframe.
    EXPECT_TRUE(NavigateToURL(shell(), url));

    // Check the FrameNavigationEntry's referrer.
    NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
    ASSERT_EQ(1U, entry->root_node()->children.size());
    scoped_refptr<FrameNavigationEntry> frame_entry =
        entry->root_node()->children[0]->frame_entry.get();
    EXPECT_EQ(frame_entry->referrer().url, url);
  }
}

namespace {

class RequestMonitoringNavigationBrowserTest
    : public NavigationControllerBrowserTest {
 public:
  RequestMonitoringNavigationBrowserTest() = default;

  const net::test_server::HttpRequest* FindAccumulatedRequest(
      const GURL& url_to_find) {
    DCHECK(url_to_find.SchemeIsHTTPOrHTTPS());

    auto it = base::ranges::find(accumulated_requests_, url_to_find,
                                 &net::test_server::HttpRequest::GetURL);
    if (it == accumulated_requests_.end())
      return nullptr;
    return &*it;
  }

 protected:
  void SetUpOnMainThread() override {
    // Accumulate all http requests made to |embedded_test_server| into
    // |accumulated_requests_| container.
    embedded_test_server()->RegisterRequestMonitor(base::BindRepeating(
        &RequestMonitoringNavigationBrowserTest::MonitorRequestOnIoThread,
        weak_factory_.GetWeakPtr(),
        base::SequencedTaskRunner::GetCurrentDefault()));

    ASSERT_TRUE(embedded_test_server()->Start());
  }

  void TearDown() override {
    EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
  }

 private:
  static void MonitorRequestOnIoThread(
      const base::WeakPtr<RequestMonitoringNavigationBrowserTest>& weak_this,
      const scoped_refptr<base::SequencedTaskRunner>& postback_task_runner,
      const net::test_server::HttpRequest& request) {
    postback_task_runner->PostTask(
        FROM_HERE,
        base::BindOnce(
            &RequestMonitoringNavigationBrowserTest::MonitorRequestOnMainThread,
            weak_this, request));
  }

  void MonitorRequestOnMainThread(
      const net::test_server::HttpRequest& request) {
    accumulated_requests_.push_back(request);
  }

  std::vector<net::test_server::HttpRequest> accumulated_requests_;
  // Must be last member.
  base::WeakPtrFactory<RequestMonitoringNavigationBrowserTest> weak_factory_{
      this};
};

// Helper for waiting until the main frame of |web_contents| has loaded
// |expected_url| (and all subresources have finished loading).
class WebContentsLoadFinishedWaiter : public WebContentsObserver {
 public:
  WebContentsLoadFinishedWaiter(WebContents* web_contents,
                                const GURL& expected_url)
      : WebContentsObserver(web_contents),
        expected_url_(expected_url),
        message_loop_runner_(new MessageLoopRunner) {
    EXPECT_TRUE(web_contents != nullptr);
  }

  void Wait() { message_loop_runner_->Run(); }

 private:
  void DidFinishLoad(RenderFrameHost* render_frame_host,
                     const GURL& url) override {
    bool is_main_frame = !render_frame_host->GetParent();
    if (url == expected_url_ && is_main_frame)
      message_loop_runner_->Quit();
  }

  GURL expected_url_;
  scoped_refptr<MessageLoopRunner> message_loop_runner_;
};

}  // namespace

// Check that NavigationController::LoadURLParams::extra_headers are not copied
// to subresource requests.
IN_PROC_BROWSER_TEST_P(RequestMonitoringNavigationBrowserTest,
                       ExtraHeadersVsSubresources) {
  GURL page_url = embedded_test_server()->GetURL("/page_with_image.html");
  GURL image_url = embedded_test_server()->GetURL("/blank.jpg");

  // Navigate via LoadURLWithParams (setting |extra_headers| field).
  WebContentsLoadFinishedWaiter waiter(shell()->web_contents(), page_url);
  NavigationController::LoadURLParams load_url_params(page_url);
  load_url_params.extra_headers =
      "X-ExtraHeadersVsSubresources: 1\n"
      "X-2ExtraHeadersVsSubresources: 2";
  shell()->web_contents()->GetController().LoadURLWithParams(load_url_params);
  waiter.Wait();
  EXPECT_EQ(page_url, shell()->web_contents()->GetLastCommittedURL());

  // Verify that the extra header was present for the page.
  const net::test_server::HttpRequest* page_request =
      FindAccumulatedRequest(page_url);
  ASSERT_TRUE(page_request);
  EXPECT_THAT(page_request->headers,
              testing::Contains(testing::Key("X-ExtraHeadersVsSubresources")));
  EXPECT_THAT(page_request->headers,
              testing::Contains(testing::Key("X-2ExtraHeadersVsSubresources")));

  // Verify that the extra header was NOT present for the subresource.
  const net::test_server::HttpRequest* image_request =
      FindAccumulatedRequest(image_url);
  ASSERT_TRUE(image_request);
  EXPECT_THAT(image_request->headers,
              testing::Not(testing::Contains(
                  testing::Key("X-ExtraHeadersVsSubresources"))));
  EXPECT_THAT(image_request->headers,
              testing::Not(testing::Contains(
                  testing::Key("X-2ExtraHeadersVsSubresources"))));
}

// Test that a same document navigation does not lead to the deletion of the
// NavigationHandle for an ongoing different document navigation.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SameDocumentNavigationDoesntDeleteNavigationHandle) {
  const GURL kURL1 = embedded_test_server()->GetURL("/title1.html");
  const GURL kPushStateURL =
      embedded_test_server()->GetURL("/title1.html#fragment");
  const GURL kURL2 = embedded_test_server()->GetURL("/title2.html");

  // Navigate to the initial page.
  EXPECT_TRUE(NavigateToURL(shell(), kURL1));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_FALSE(root->navigation_request());

  // Start navigating to the second page.
  TestNavigationManager manager(shell()->web_contents(), kURL2);
  NavigationHandleCommitObserver navigation_observer(shell()->web_contents(),
                                                     kURL2);
  shell()->web_contents()->GetController().LoadURL(
      kURL2, Referrer(), ui::PAGE_TRANSITION_LINK, std::string());
  EXPECT_TRUE(manager.WaitForRequestStart());

  // This should create a NavigationHandle.
  NavigationRequest* request = root->navigation_request();
  EXPECT_TRUE(request);

  // The current page does a PushState.
  NavigationHandleCommitObserver push_state_observer(shell()->web_contents(),
                                                     kPushStateURL);
  std::string push_state =
      JsReplace("history.pushState({}, 'title 1', $1);", kPushStateURL);
  EXPECT_TRUE(ExecJs(shell()->web_contents(), push_state));
  NavigationEntry* last_committed =
      shell()->web_contents()->GetController().GetLastCommittedEntry();
  ASSERT_TRUE(last_committed);
  EXPECT_EQ(kPushStateURL, last_committed->GetURL());

  EXPECT_TRUE(push_state_observer.has_committed());
  EXPECT_TRUE(push_state_observer.was_same_document());
  EXPECT_TRUE(push_state_observer.was_renderer_initiated());

  // This shouldn't affect the ongoing navigation.
  EXPECT_TRUE(root->navigation_request());
  EXPECT_EQ(request, root->navigation_request());

  // Let the navigation finish. It should commit successfully.
  ASSERT_TRUE(manager.WaitForNavigationFinished());
  last_committed =
      shell()->web_contents()->GetController().GetLastCommittedEntry();
  ASSERT_TRUE(last_committed);
  EXPECT_EQ(kURL2, last_committed->GetURL());

  EXPECT_TRUE(navigation_observer.has_committed());
  EXPECT_FALSE(navigation_observer.was_same_document());
  EXPECT_FALSE(navigation_observer.was_renderer_initiated());
}

// Tests that a same document browser-initiated navigation is properly reported
// by the NavigationHandle.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SameDocumentBrowserInitiated) {
  const GURL kURL = embedded_test_server()->GetURL("/title1.html");
  const GURL kFragmentURL =
      embedded_test_server()->GetURL("/title1.html#fragment");

  // Navigate to the initial page.
  EXPECT_TRUE(NavigateToURL(shell(), kURL));

  // Do a browser-initiated fragment navigation.
  NavigationHandleCommitObserver handle_observer(shell()->web_contents(),
                                                 kFragmentURL);
  EXPECT_TRUE(NavigateToURL(shell(), kFragmentURL));

  EXPECT_TRUE(handle_observer.has_committed());
  EXPECT_TRUE(handle_observer.was_same_document());
  EXPECT_FALSE(handle_observer.was_renderer_initiated());
}

// Tests that a 204 response to a browser-initiated navigation does not result
// in a new NavigationEntry being committed.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, 204Navigation) {
  const GURL kURL = embedded_test_server()->GetURL("/title1.html");
  const GURL kURL204 = embedded_test_server()->GetURL("/page204.html");

  // Navigate to the initial page.
  EXPECT_TRUE(NavigateToURL(shell(), kURL));

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(kURL, entry->GetURL());
  EXPECT_EQ(1, controller.GetEntryCount());

  // Do a 204 navigation.
  EXPECT_FALSE(NavigateToURL(shell(), kURL204));

  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(kURL, entry->GetURL());
  EXPECT_EQ(1, controller.GetEntryCount());
}

// Tests that navigating to an empty URL or a URL that never commits will
// have appropriate origins. Empty URL navigations are not currently known to
// be possible, but it used to be possible and caused crashes because the
// initiator origin wasn't inherited correctly with empty URLs. See also
// https://crbug.com/1240138.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, NavigateToEmptyURL) {
  GURL url_1 = embedded_test_server()->GetURL("/title1.html");
  GURL url_2 = embedded_test_server()->GetURL("/title2.html");
  GURL url_204 = embedded_test_server()->GetURL("/page204.html");

  // Navigate to the initial page.
  EXPECT_TRUE(NavigateToURL(shell(), url_1));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(url_1, entry->GetURL());
  EXPECT_EQ(1, controller.GetEntryCount());

  // Navigate (browser-initiated) to an empty URL, which will get rewritten to
  // about:blank and commit an opaque origin.
  EXPECT_TRUE(NavigateToURL(shell(), GURL(), GURL("about:blank")));

  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(GURL("about:blank"), entry->GetURL());
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_TRUE(
      contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin().opaque());
  EXPECT_FALSE(entry->root_node()->frame_entry->initiator_origin().has_value());

  // Trying to navigate to an empty URL through setting the location would fail
  // because it's not a valid URL and it's stopped in the renderer.
  EXPECT_FALSE(NavigateToURLFromRenderer(shell(), GURL()));
  EXPECT_EQ(entry, controller.GetLastCommittedEntry());
  EXPECT_EQ(2, controller.GetEntryCount());

  // Navigate another URL.
  EXPECT_TRUE(NavigateToURL(shell(), url_2));
  const url::Origin& opener_origin =
      contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin();

  {
    // Pop open a new window which won't commit a navigation, so it will stay at
    // the initial NavigationEntry.
    Shell* new_shell = OpenWindow(contents(), url_204);
    WebContentsImpl* new_contents =
        static_cast<WebContentsImpl*>(new_shell->web_contents());

    // The initial NavigationEntry will have an empty URL, and its initiator
    // origin is set to the origin of the document that opened the window.
    NavigationControllerImpl& new_controller = new_contents->GetController();
    EXPECT_EQ(1, new_controller.GetEntryCount());
    entry = new_controller.GetLastCommittedEntry();
    EXPECT_TRUE(entry->IsInitialEntry());
    EXPECT_EQ(GURL(), entry->GetURL());

    scoped_refptr<FrameNavigationEntry> frame_entry =
        entry->root_node()->frame_entry.get();
    ASSERT_TRUE(frame_entry->initiator_origin().has_value());
    EXPECT_EQ(opener_origin, frame_entry->initiator_origin().value());
    EXPECT_EQ(opener_origin, new_shell->web_contents()
                                 ->GetPrimaryMainFrame()
                                 ->GetLastCommittedOrigin());
  }

  {
    // Pop open a new window which won't commit a navigation, but this time use
    // the 'noopener' option, which will cause the navigation to be triggered
    // by the browser.
    ShellAddedObserver new_shell_observer;
    EXPECT_TRUE(
        ExecJs(shell(), "window.open('/page204.html', '_blank', 'noopener');"));
    Shell* new_shell = new_shell_observer.GetShell();

    // The initial NavigationEntry will have an empty URL, and its initiator
    // origin is set to an opaque origin (not the opener's origin).
    NavigationControllerImpl& new_controller =
        static_cast<WebContentsImpl*>(new_shell->web_contents())
            ->GetController();
    EXPECT_EQ(1, new_controller.GetEntryCount());
    entry = new_controller.GetLastCommittedEntry();
    EXPECT_TRUE(entry->IsInitialEntry());
    EXPECT_EQ(GURL(), entry->GetURL());

    scoped_refptr<FrameNavigationEntry> frame_entry =
        entry->root_node()->frame_entry.get();
    ASSERT_TRUE(frame_entry->initiator_origin().has_value());
    EXPECT_NE(opener_origin, frame_entry->initiator_origin().value());
    EXPECT_TRUE(frame_entry->initiator_origin()->opaque());
    EXPECT_EQ(frame_entry->initiator_origin().value(),
              new_shell->web_contents()
                  ->GetPrimaryMainFrame()
                  ->GetLastCommittedOrigin());
  }

  {
    // Pop open another window,  this time to an empty URL and with the
    // 'noopener' option. This navigation will go through the browser and the
    // empty URL will be rewritten to about:blank#blocked, but the initiator
    // origin is still set to the opener's origin.
    ShellAddedObserver new_shell_observer;
    EXPECT_TRUE(ExecJs(shell(), "window.open('', '_blank', 'noopener');"));
    Shell* new_shell = new_shell_observer.GetShell();
    EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));

    NavigationControllerImpl& new_controller =
        static_cast<WebContentsImpl*>(new_shell->web_contents())
            ->GetController();
    EXPECT_EQ(1, new_controller.GetEntryCount());
    entry = new_controller.GetLastCommittedEntry();
    EXPECT_FALSE(entry->IsInitialEntry());
    EXPECT_EQ(GURL("about:blank#blocked"), entry->GetURL());

    scoped_refptr<FrameNavigationEntry> frame_entry =
        entry->root_node()->frame_entry.get();
    ASSERT_TRUE(frame_entry->initiator_origin().has_value());
    EXPECT_EQ(opener_origin, frame_entry->initiator_origin().value());
    EXPECT_EQ(opener_origin, new_shell->web_contents()
                                 ->GetPrimaryMainFrame()
                                 ->GetLastCommittedOrigin());
  }
}

// Tests that stopping a load clears the pending navigation entry.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, StopDuringLoad) {
  // Load an initial page since the behavior differs for the first entry.
  GURL start_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
  GURL slow_url = embedded_test_server()->GetURL("/slow?60");
  shell()->LoadURL(slow_url);
  shell()->web_contents()->Stop();

  NavigationController& controller = shell()->web_contents()->GetController();
  ASSERT_EQ(controller.GetPendingEntry(), nullptr);
}

// Tests that reloading a page that has no title doesn't inherit the title from
// the previous version of the page.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, ReloadDoesntKeepTitle) {
  NavigationController& controller = shell()->web_contents()->GetController();
  GURL start_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  GURL intermediate_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_2.html"));
  std::u16string title = u"title";

  // Reload from the browser side.
  {
    EXPECT_TRUE(NavigateToURL(shell(), start_url));

    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(entry->GetTitle().empty());
    entry->SetTitle(title);

    controller.Reload(ReloadType::NORMAL, false);
    EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

    EXPECT_TRUE(entry->GetTitle().empty());
  }

  // Load an unrelated page; this disconnects these two tests.
  EXPECT_TRUE(NavigateToURL(shell(), intermediate_url));

  // Reload from the renderer side.
  {
    EXPECT_TRUE(NavigateToURL(shell(), start_url));

    NavigationEntry* entry = controller.GetLastCommittedEntry();
    EXPECT_TRUE(entry->GetTitle().empty());
    entry->SetTitle(title);

    TestNavigationObserver reload_observer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(shell(), "location.reload()"));
    reload_observer.Wait();

    EXPECT_TRUE(entry->GetTitle().empty());
  }

  // Load an unrelated page; this disconnects these two tests.
  EXPECT_TRUE(NavigateToURL(shell(), intermediate_url));

  // "Reload" by loading the same page again.
  {
    EXPECT_TRUE(NavigateToURL(shell(), start_url));

    NavigationEntry* entry1 = controller.GetLastCommittedEntry();
    EXPECT_TRUE(entry1->GetTitle().empty());
    entry1->SetTitle(title);

    EXPECT_TRUE(NavigateToURL(shell(), start_url));
    NavigationEntry* entry2 = controller.GetLastCommittedEntry();

    EXPECT_EQ(entry1, entry2);
    EXPECT_TRUE(entry1->GetTitle().empty());
  }
}

// Verify that session history navigations (back/forward) correctly hit the
// cache instead of going to the server. The test loads a page with no-cache
// header, stops the server, and goes back expecting successful navigation.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       HistoryNavigationUsesCache) {
  GURL no_cache_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_no_cache_header.html"));
  GURL regular_url(embedded_test_server()->GetURL("/title2.html"));

  NavigationController& controller = shell()->web_contents()->GetController();

  EXPECT_TRUE(NavigateToURL(shell(), no_cache_url));
  EXPECT_TRUE(NavigateToURL(shell(), regular_url));
  EXPECT_EQ(2, controller.GetEntryCount());

  EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());

  TestNavigationObserver back_observer(shell()->web_contents());
  controller.GoBack();
  back_observer.Wait();

  EXPECT_TRUE(back_observer.last_navigation_succeeded());
}

// Test to verify that navigating to a blocked URL does not result in a
// NavigationEntry that allows the navigation to succeed when using a history
// navigation. See https://crbug.com/723796.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       VerifyBlockedErrorPageURL_SessionHistory) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());

  // Navigate to a URL that is blocked, which results in an error page.
  GURL blocked_url(embedded_test_server()->GetURL("/blocked.html"));
  std::unique_ptr<URLLoaderInterceptor> url_interceptor =
      URLLoaderInterceptor::SetupRequestFailForURL(blocked_url,
                                                   net::ERR_BLOCKED_BY_CLIENT);
  EXPECT_FALSE(NavigateToURL(shell(), blocked_url));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(PAGE_TYPE_ERROR, controller.GetLastCommittedEntry()->GetPageType());

  // Navigate to a new document, then go back in history trying to load the
  // blocked URL.
  EXPECT_TRUE(
      NavigateToURL(shell(), embedded_test_server()->GetURL("/title1.html")));
  EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());

  TestNavigationObserver back_load_observer(shell()->web_contents());
  controller.GoBack();
  back_load_observer.Wait();

  // The expectation is that the blocked URL is present in the NavigationEntry,
  // and shows up in both GetURL and GetVirtualURL.
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(blocked_url, controller.GetLastCommittedEntry()->GetURL());
  EXPECT_EQ(blocked_url, controller.GetLastCommittedEntry()->GetVirtualURL());
}

// Verifies that unsafe redirects to javascript: URLs are canceled and don't
// make a spoof possible. Ideally they would create an error page, but some
// extensions rely on them being silently blocked. See https://crbug.com/935175
// and https://crbug.com/941653.
// This test is flaky on Linux : http://crbug.com/1223051
#if BUILDFLAG(IS_LINUX)
#define MAYBE_JavascriptRedirectSilentlyCanceled \
  DISABLED_JavascriptRedirectSilentlyCanceled
#else
#define MAYBE_JavascriptRedirectSilentlyCanceled \
  JavascriptRedirectSilentlyCanceled
#endif
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       MAYBE_JavascriptRedirectSilentlyCanceled) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());

  // Navigating to a URL that redirects to a javascript: URL doesn't create an
  // error page; the navigation is simply ignored. Check the pending URL is not
  // left in the address bar.
  GURL redirect_to_unsafe_url(
      embedded_test_server()->GetURL("/server-redirect?javascript:Hello!"));
  EXPECT_FALSE(NavigateToURL(shell(), redirect_to_unsafe_url));
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(PAGE_TYPE_NORMAL,
            controller.GetLastCommittedEntry()->GetPageType());
  EXPECT_EQ(controller.GetVisibleEntry(), controller.GetLastCommittedEntry());
  EXPECT_EQ(start_url, controller.GetVisibleEntry()->GetURL());
}

// Verifies that redirecting to a blocked URL and going back does not allow a
// URL spoof.  See https://crbug.com/777419.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       PreventSpoofFromBlockedRedirect) {
  GURL url1 = embedded_test_server()->GetURL(
      "a.com", "/navigation_controller/simple_page_1.html");
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Pop open a new window.
  ShellAddedObserver new_shell_observer;
  EXPECT_TRUE(ExecJs(root, "var w = window.open()"));
  Shell* new_shell = new_shell_observer.GetShell();
  ASSERT_NE(new_shell->web_contents(), shell()->web_contents());

  // Navigate it to a cross-site URL that redirects to a data: URL.  Since it is
  // an unsafe redirect, it will result in a blocked navigation and error page.
  GURL redirect_to_data_url(
      embedded_test_server()->GetURL("/server-redirect?data:text/html,Hello!"));
  TestNavigationObserver nav_observer(new_shell->web_contents(), 1);
  EXPECT_TRUE(
      ExecJs(root, "w.location.href = '" + redirect_to_data_url.spec() + "';"));
  nav_observer.WaitForNavigationFinished();
  EXPECT_FALSE(nav_observer.last_navigation_succeeded());
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      new_shell->web_contents()->GetController());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(redirect_to_data_url, controller.GetLastCommittedEntry()->GetURL());
  EXPECT_EQ(PAGE_TYPE_ERROR, controller.GetLastCommittedEntry()->GetPageType());

  // Navigate to a new document, then go back in history trying to load the
  // blocked URL.
  EXPECT_TRUE(NavigateToURL(new_shell, url1));
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url1, controller.GetLastCommittedEntry()->GetURL());
  TestNavigationObserver back_load_observer(new_shell->web_contents());
  controller.GoBack();
  back_load_observer.Wait();
  EXPECT_EQ(redirect_to_data_url, controller.GetLastCommittedEntry()->GetURL());

  // The opener should not be able to script the page, which should be another
  // error message and not a blank page.
  std::string result = EvalJs(shell(),
                              "try {\n"
                              "  w.document.body.innerHTML;\n"
                              "} catch (e) {\n"
                              "  e.toString();\n"
                              "}")
                           .ExtractString();
  DLOG(INFO) << "Result: " << result;
  EXPECT_THAT(result,
              ::testing::MatchesRegex("SecurityError: Blocked a frame with "
                                      "origin \"http://a.com:\\d+\" from "
                                      "accessing a cross-origin frame."));
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SameDocumentNavAfterJavaScriptURL) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // Navigate to |start_url|.
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
  EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  const int64_t start_dsn = controller.GetLastCommittedEntry()
                                ->GetFrameEntry(root)
                                ->document_sequence_number();

  // Do a javascript: URL "navigation", which will create a new document but
  // won't send anything to the browser.
  EXPECT_TRUE(ExecJs(root, R"(window.location = 'javascript:"foo"';)"));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(start_url, root->current_url());
  EXPECT_EQ("foo", EvalJs(shell(), "document.body.innerHTML"));
  EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
  EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
  EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                           ->GetFrameEntry(root)
                           ->document_sequence_number());

  // Do a same-document renderer-initiated fragment navigation, which should
  // retain the HTTP method and status code.
  GURL fragment_url = embedded_test_server()->GetURL("/title1.html#bar");
  {
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "location.href='#bar';"));
    capturer.Wait();
    EXPECT_NE(start_url, root->current_url());
    EXPECT_TRUE(capturer.is_same_document());
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
    EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
    EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                             ->GetFrameEntry(root)
                             ->document_sequence_number());
  }

  // Go back. This should be a same-document navigation and retain the HTTP
  // method and status code.
  {
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.go(-1);"));
    capturer.Wait();
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ(start_url, root->current_url());
    EXPECT_TRUE(capturer.is_same_document());
    EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
    EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
    EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                             ->GetFrameEntry(root)
                             ->document_sequence_number());
  }
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       BrowserInitiatedSameDocumentNavAfterJavaScriptURL) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // Navigate to |start_url|.
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
  EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  const int64_t start_dsn = controller.GetLastCommittedEntry()
                                ->GetFrameEntry(root)
                                ->document_sequence_number();

  // Do a javascript: URL "navigation", which will create a new document but
  // won't send anything to the browser.
  EXPECT_TRUE(ExecJs(root, R"(window.location = 'javascript:"foo"';)"));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(start_url, root->current_url());
  EXPECT_EQ("foo", EvalJs(shell(), "document.body.innerHTML"));
  EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
  EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
  EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                           ->GetFrameEntry(root)
                           ->document_sequence_number());

  // Do a same-document browser-initiated fragment navigation, which should
  // retain the HTTP method and status code.
  GURL fragment_url = embedded_test_server()->GetURL("/title1.html#bar");
  {
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(NavigateToURL(shell(), fragment_url));
    capturer.Wait();
    EXPECT_EQ(fragment_url, root->current_url());
    EXPECT_TRUE(capturer.is_same_document());
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
    EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
    EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                             ->GetFrameEntry(root)
                             ->document_sequence_number());
  }

  // Go back. This should be a same-document navigation and retain the HTTP
  // method and status code.
  {
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.go(-1);"));
    capturer.Wait();
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ(start_url, root->current_url());
    EXPECT_TRUE(capturer.is_same_document());
    EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
    EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
    EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                             ->GetFrameEntry(root)
                             ->document_sequence_number());
  }
}

// Does a same-document navigation on an iframe after its document gets
// replaced.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SameDocumentNavAfterDocumentReplaceChild) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // Navigate to a page with an iframe.
  GURL main_url(embedded_test_server()->GetURL("/page_with_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
  EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  FrameTreeNode* iframe = root->child_at(0);
  const int64_t start_dsn = controller.GetLastCommittedEntry()
                                ->GetFrameEntry(iframe)
                                ->document_sequence_number();
  GURL iframe_url = iframe->current_url();

  // Create a new document with document.implementation.createHTMLDocument that
  // will replace the iframe's document, but won't notify the browser.
  EXPECT_TRUE(ExecJs(root, R"(
    let newDoc = document.implementation.createHTMLDocument();
    newDoc.body.innerHTML = "foo";
    let frame = document.getElementById("test_iframe");
    let destDocument = frame.contentDocument;
    let newNode = destDocument.importNode(newDoc.documentElement, true);
    destDocument.replaceChild(newNode, destDocument.documentElement);
  )"));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(iframe_url, iframe->current_url());
  EXPECT_EQ("foo", EvalJs(iframe, "document.body.innerHTML"));
  EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
  EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
  EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                           ->GetFrameEntry(iframe)
                           ->document_sequence_number());

  // Do a same-document renderer-initiated fragment navigation, which should
  // retain the HTTP method and status code.
  GURL fragment_url = embedded_test_server()->GetURL("/title1.html#bar");
  {
    FrameNavigateParamsCapturer capturer(iframe);
    EXPECT_TRUE(ExecJs(iframe, "location.href='#bar';"));
    capturer.Wait();
    EXPECT_EQ(iframe_url.spec() + "#bar", iframe->current_url().spec());
    EXPECT_TRUE(capturer.is_same_document());
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
    EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
    EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                             ->GetFrameEntry(iframe)
                             ->document_sequence_number());
  }

  // Go back. This should be a same-document navigation and retain the HTTP
  // method and status code.
  {
    FrameNavigateParamsCapturer capturer(iframe);
    EXPECT_TRUE(ExecJs(iframe, "history.go(-1);"));
    capturer.Wait();
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ(iframe_url, iframe->current_url());
    EXPECT_TRUE(capturer.is_same_document());
    EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
    EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
    EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                             ->GetFrameEntry(iframe)
                             ->document_sequence_number());
  }
}

// Does a same-document navigation on an iframe after its document gets
// replaced.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SameDocumentNavAfterXSL) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // Navigate to an XML page.
  GURL start_url(embedded_test_server()->GetURL("/permissions-policy.xml"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
  EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  const int64_t start_dsn = controller.GetLastCommittedEntry()
                                ->GetFrameEntry(root)
                                ->document_sequence_number();
  // Wait until the page has fully loaded, which will trigger an XSLT document
  // change in the renderer.
  EXPECT_TRUE(WaitForLoadStop(contents()));

  // Do a same-document browser-initiated fragment navigation, which should
  // retain the HTTP method and status code.
  GURL fragment_url =
      embedded_test_server()->GetURL("/permissions-policy.xml#bar");
  {
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(NavigateToURL(shell(), fragment_url));
    capturer.Wait();
    EXPECT_EQ(fragment_url, root->current_url());
    EXPECT_TRUE(capturer.is_same_document());
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
    EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
    EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                             ->GetFrameEntry(root)
                             ->document_sequence_number());
  }

  // Go back. This should be a same-document navigation and retain the HTTP
  // method and status code.
  {
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.go(-1);"));
    capturer.Wait();
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ(start_url, root->current_url());
    EXPECT_TRUE(capturer.is_same_document());
    EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
    EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
    EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                             ->GetFrameEntry(root)
                             ->document_sequence_number());
  }
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SameDocumentNavAfterJavaScriptURLOn404Page) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // Navigate to |start_url|.
  GURL start_url(embedded_test_server()->GetURL("/page404.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
  EXPECT_EQ(404, contents()->GetPrimaryMainFrame()->last_http_status_code());

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  const int64_t start_dsn = controller.GetLastCommittedEntry()
                                ->GetFrameEntry(root)
                                ->document_sequence_number();

  // Do a javascript: URL "navigation", which will create a new document but
  // won't send anything to the browser.
  EXPECT_TRUE(ExecJs(root, R"(window.location = 'javascript:"foo"';)"));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(start_url, root->current_url());
  EXPECT_EQ("foo", EvalJs(shell(), "document.body.innerHTML"));
  EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
  EXPECT_EQ(404, contents()->GetPrimaryMainFrame()->last_http_status_code());
  EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                           ->GetFrameEntry(root)
                           ->document_sequence_number());

  // Do a same-document renderer-initiated fragment navigation, which should
  // retain the HTTP method and status code.
  GURL fragment_url = embedded_test_server()->GetURL("/page404.html#bar");
  {
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "location.href='#bar';"));
    capturer.Wait();
    EXPECT_NE(start_url, root->current_url());
    EXPECT_TRUE(capturer.is_same_document());
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
    EXPECT_EQ(404, contents()->GetPrimaryMainFrame()->last_http_status_code());
    EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                             ->GetFrameEntry(root)
                             ->document_sequence_number());
  }

  // Go back. This should be a same-document navigation and retain the HTTP
  // method and status code.
  {
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.go(-1);"));
    capturer.Wait();
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ(start_url, root->current_url());
    EXPECT_TRUE(capturer.is_same_document());
    EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
    EXPECT_EQ(404, contents()->GetPrimaryMainFrame()->last_http_status_code());
    EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                             ->GetFrameEntry(root)
                             ->document_sequence_number());
  }
}

// Same-document navigations can sometimes succeed but then later be blocked by
// policy (e.g., X-Frame-Options) after a page is restored or reloaded.  Ensure
// that navigating back from a newly blocked URL in a subframe is not treated as
// same-document, even if it had been same-document originally.
// See https://crbug.com/765291.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       BackSameDocumentAfterBlockedSubframe) {
  // The test assumes the previous iframe gets deleted after navigation and
  // later recreated on history navigations. Disable back/forward cache to
  // ensure that it doesn't get preserved in the cache.
  DisableBackForwardCacheForTesting(shell()->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // 1) Navigate to a page with an iframe.
  GURL start_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe_simple.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  GURL original_child_url = root->child_at(0)->current_url();

  // 2) pushState to a URL that will be blocked by XFO if loaded from scratch.
  GURL x_frame_options_deny_url =
      embedded_test_server()->GetURL("/x-frame-options-deny.html");
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    std::string pushStateToXfo =
        "history.pushState({}, '', '/x-frame-options-deny.html')";
    EXPECT_TRUE(ExecJs(root->child_at(0), pushStateToXfo));
    capturer.Wait();
    EXPECT_EQ(x_frame_options_deny_url, root->child_at(0)->current_url());
    EXPECT_TRUE(capturer.is_same_document());
  }

  // 3) Navigate the main frame to another page.
  GURL new_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), new_url));

  // 4) Go back, causing the subframe to be blocked by XFO.
  {
    TestNavigationObserver observer(shell()->web_contents());
    controller.GoBack();
    observer.Wait();
    EXPECT_EQ(start_url, root->current_url());
    EXPECT_EQ(x_frame_options_deny_url, root->child_at(0)->current_url());
    EXPECT_EQ(net::ERR_BLOCKED_BY_RESPONSE, observer.last_net_error_code());
    EXPECT_FALSE(observer.last_navigation_succeeded());
  }

  // 5) Go back again.  This would have been same-document if the prior
  // navigation had succeeded, but we did a cross-document navigation instead
  // because the previous page is an error page.
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    TestNavigationObserver observer(shell()->web_contents());
    controller.GoBack();
    capturer.Wait();
    observer.Wait();
    EXPECT_FALSE(capturer.is_same_document());
    EXPECT_EQ(start_url, root->current_url());
    EXPECT_EQ(original_child_url, root->child_at(0)->current_url());
    EXPECT_TRUE(observer.last_navigation_succeeded());
    EXPECT_EQ(net::OK, observer.last_net_error_code());
  }

  // 6) Go forward two steps. This would load the page from step 3.
  {
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.go(2)"));
    capturer.Wait();
    EXPECT_EQ(new_url, root->current_url());
    EXPECT_FALSE(capturer.is_same_document());
  }

  // 7) Go back two steps. This would load the page from step 1, with the iframe
  // loaded to the original URL from step 1.
  {
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.go(-2)"));
    capturer.Wait();
    EXPECT_EQ(start_url, root->current_url());
    EXPECT_EQ(original_child_url, root->child_at(0)->current_url());
    EXPECT_FALSE(capturer.is_same_document());
  }

  // 8) Go forward one step. This would do a same document navigation, with the
  // iframe still loaded to the original URL from step 1, but the URL is updated
  // to the XFO URL.
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    TestNavigationObserver observer(shell()->web_contents());
    controller.GoForward();
    capturer.Wait();
    observer.Wait();
    EXPECT_EQ(start_url, root->current_url());
    EXPECT_EQ(x_frame_options_deny_url, root->child_at(0)->current_url());
    if (!SiteIsolationPolicy::IsErrorPageIsolationEnabled(
            /*in_main_frame=*/false)) {
      EXPECT_TRUE(capturer.is_same_document());
      EXPECT_TRUE(observer.last_navigation_succeeded());
      EXPECT_EQ(net::OK, observer.last_net_error_code());
    } else {
      EXPECT_FALSE(capturer.is_same_document());
      EXPECT_FALSE(observer.last_navigation_succeeded());
      EXPECT_EQ(net::ERR_BLOCKED_BY_RESPONSE, observer.last_net_error_code());
    }
  }

  // Check that the renderer is still alive.
  EXPECT_TRUE(ExecJs(root->child_at(0), "console.log('Success');"));
}

// Similar to BackSameDocumentAfterBlockedSubframe but does the navigation on a
// main frame instead (and does a 404 instead of XFO error).
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       BackSameDocumentAfter404MainFrame) {
  // This test expects going back to trigger a new page load and fetch a URL
  // (which would fail with a 404 error). Disable back/forward cache to ensure
  // that it doesn't happen.
  DisableBackForwardCacheForTesting(shell()->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  // 1) Navigate to |start_url|.
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 2) pushState to a URL that will 404 & result in Chrome's error page if
  // loaded from scratch.
  GURL error_url = embedded_test_server()->GetURL("/empty404.html");
  {
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.pushState({}, '', '/empty404.html')"));
    capturer.Wait();
    EXPECT_EQ(error_url, root->current_url());
    EXPECT_TRUE(capturer.is_same_document());
  }

  // 3) Navigate the main frame to another page.
  GURL new_url(embedded_test_server()->GetURL("/title2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), new_url));

  // 4) Go back. This will 404 so we will show an error page.
  {
    TestNavigationObserver observer(shell()->web_contents());
    controller.GoBack();
    observer.Wait();
    EXPECT_EQ(error_url, root->current_url());
    EXPECT_EQ(net::ERR_HTTP_RESPONSE_CODE_FAILURE,
              observer.last_net_error_code());
    EXPECT_FALSE(observer.last_navigation_succeeded());
  }

  // 5) Go back again.  This would have been same-document if the prior
  // navigation had succeeded, but we did a cross-document navigation instead
  // because the previous page is an error page.
  {
    FrameNavigateParamsCapturer capturer(root);
    TestNavigationObserver observer(shell()->web_contents());
    controller.GoBack();
    capturer.Wait();
    observer.Wait();
    EXPECT_EQ(start_url, root->current_url());
    EXPECT_TRUE(observer.last_navigation_succeeded());
    EXPECT_EQ(net::OK, observer.last_net_error_code());
    EXPECT_FALSE(capturer.is_same_document());
  }

  // Check that the renderer is still alive.
  EXPECT_TRUE(ExecJs(root, "console.log('Success');"));
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       HistoryAPIHistoryNavigation) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1) Navigate to |start_url|.
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  const int64_t start_dsn = controller.GetLastCommittedEntry()
                                ->GetFrameEntry(root)
                                ->document_sequence_number();

  // 2) pushState a different-document URL. This will be classified as a
  // same-document navigation and will keep the previous Document Sequence
  // Number.
  GURL push_state_url(embedded_test_server()->GetURL("/title2.html"));
  {
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.pushState({}, '', '/title2.html')"));
    capturer.Wait();
    EXPECT_TRUE(capturer.is_same_document());
    EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                             ->GetFrameEntry(root)
                             ->document_sequence_number());
  }

  // 3) Navigate to another page. This will be classified as a cross-document
  // navigation and will change the Document Sequence Number.
  GURL end_url(embedded_test_server()->GetURL("/title3.html"));
  EXPECT_TRUE(NavigateToURL(shell(), end_url));
  EXPECT_NE(start_dsn, controller.GetLastCommittedEntry()
                           ->GetFrameEntry(root)
                           ->document_sequence_number());

  // 4) Go back. This will load the URL from pushState. This will be classified
  // as a cross-document history navigation, and will use the Document Sequence
  // Number from the FrameNavigationEntry.
  {
    FrameNavigateParamsCapturer capturer(root);
    TestNavigationObserver observer(shell()->web_contents());
    controller.GoBack();
    capturer.Wait();
    observer.Wait();
    EXPECT_FALSE(capturer.is_same_document());
    EXPECT_EQ(push_state_url, root->current_url());
    EXPECT_TRUE(observer.last_navigation_succeeded());
    EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                             ->GetFrameEntry(root)
                             ->document_sequence_number());
    // Set a variable in this document.
    EXPECT_TRUE(ExecJs(shell(), "var foo = 42;"));
  }

  // 5) Go back. This will load the starting URL but it's still on the same
  // document as the one loaded in the last navigation, due to the
  // FrameNavigationEntry having the same Document Sequence Number as the
  // previous FrameNavigationEntry.
  {
    FrameNavigateParamsCapturer capturer(root);
    TestNavigationObserver observer(shell()->web_contents());
    controller.GoBack();
    capturer.Wait();
    observer.Wait();
    EXPECT_TRUE(capturer.is_same_document());
    EXPECT_EQ(start_url, root->current_url());
    EXPECT_TRUE(observer.last_navigation_succeeded());
    EXPECT_EQ(start_dsn, controller.GetLastCommittedEntry()
                             ->GetFrameEntry(root)
                             ->document_sequence_number());
    // The variable set in the document at step 4 can be accessed because we did
    // a same-document navigation.
    EXPECT_EQ(42, EvalJs(shell(), "foo"));
  }
}

// If the main frame does a load, it should not be reported as a subframe
// navigation. This used to occur in the following case:
// 1. You're on a site with frames.
// 2. You do a subframe navigation. This was stored with transition type
//    MANUAL_SUBFRAME.
// 3. You navigate to some non-frame site.
// 4. You navigate back to the page from step 2. Since it was initially
//    MANUAL_SUBFRAME, it will be that same transition type here.
// We don't want that, because any navigation that changes the toplevel frame
// should be tracked as a toplevel navigation (this allows us to update the URL
// bar, etc).
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       GoBackToManualSubFrame) {
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  ASSERT_EQ(1U, root->child_count());
  ASSERT_NE(nullptr, root->child_at(0));

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  EXPECT_EQ(1, controller.GetEntryCount());

  {
    // Iframe initial load.
    LoadCommittedCapturer capturer(root->child_at(0));
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_1.html"));
    NavigateFrameToURL(root->child_at(0), frame_url);
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition_type(), ui::PAGE_TRANSITION_AUTO_SUBFRAME));
    EXPECT_EQ(1, controller.GetEntryCount());
  }

  {
    // Iframe manual navigation.
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    GURL frame_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_2.html"));
    NavigateFrameToURL(root->child_at(0), frame_url);
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_MANUAL_SUBFRAME));
    EXPECT_EQ(NAVIGATION_TYPE_NEW_SUBFRAME, capturer.navigation_type());
    EXPECT_EQ(2, controller.GetEntryCount());
  }

  {
    // Main frame navigation.
    FrameNavigateParamsCapturer capturer(root);
    GURL main_url_2(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_2.html"));
    NavigateFrameToURL(root, main_url_2);
    capturer.Wait();
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(), ui::PAGE_TRANSITION_LINK));
    EXPECT_EQ(3, controller.GetEntryCount());
  }

  {
    // Check the history before going back.
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        controller.GetEntryAtIndex(0)->GetTransitionType(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    // TODO(creis, arthursonzogni): The correct PageTransition is still an open
    // question. Maybe PAGE_TRANSITION_MANUAL_SUBFRAME is more appropriate.
    // Please see https://crbug.com/740461.
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        controller.GetEntryAtIndex(1)->GetTransitionType(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        controller.GetEntryAtIndex(2)->GetTransitionType(),
        ui::PAGE_TRANSITION_LINK));
  }

  {
    // Back.
    FrameNavigateParamsCapturer capturer(root);
    shell()->web_contents()->GetController().GoBack();
    capturer.Wait();
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        capturer.transition(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FORWARD_BACK |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
  }

  {
    // Check the history again.
    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        controller.GetEntryAtIndex(0)->GetTransitionType(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        controller.GetEntryAtIndex(1)->GetTransitionType(),
        ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                  ui::PAGE_TRANSITION_FORWARD_BACK |
                                  ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
    EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
        controller.GetEntryAtIndex(2)->GetTransitionType(),
        ui::PAGE_TRANSITION_LINK));
  }
}

// Regression test for https://crbug.com/845923.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       GoBackFromCrossSiteSubFrame) {
  // Navigate to a page with a cross-site frame.
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  GURL initial_subframe_url =
      root->child_at(0)->current_frame_host()->GetLastCommittedURL();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());

  // Navigate the subframe to another cross-site location
  // (this prepares for executing history.back() in a later step).
  GURL final_subframe_url =
      embedded_test_server()->GetURL("b.com", "/title1.html");
  EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), final_subframe_url));
  EXPECT_EQ(final_subframe_url,
            root->child_at(0)->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());

  // Execute |history.back()| in the subframe.
  TestNavigationObserver nav_observer(shell()->web_contents(), 1);
  EXPECT_TRUE(ExecJs(root->child_at(0), "history.back()"));
  nav_observer.Wait();
  EXPECT_EQ(initial_subframe_url,
            root->child_at(0)->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       HashNavigationVsBeforeUnloadEvent) {
  GURL main_url(embedded_test_server()->GetURL("/title1.html"));
  GURL hash_url(embedded_test_server()->GetURL("/title1.html#hash"));

  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  EXPECT_TRUE(ExecJs(shell(),
                     R"( window.addEventListener("beforeunload", function(e) {
              domAutomationController.send("beforeunload");
          });
          window.addEventListener("unload", function(e) {
              domAutomationController.send("unload");
          });
      )"));

  DOMMessageQueue message_queue(shell()->web_contents());
  std::vector<std::string> messages;
  std::string message;
  EXPECT_TRUE(NavigateToURL(shell(), hash_url));
  while (message_queue.PopMessage(&message))
    messages.push_back(message);

  // Verify that none of "beforeunload", "unload" events fired.
  EXPECT_THAT(messages, testing::IsEmpty());
}

// This test helps verify that the browser does not retain history entries
// for removed frames *if* the removed frame was created by a script.
// Such frames get a fresh, random, unique name every time they are created
// or recreated and therefore in such case will never match previous history
// entries.  See also https://crbug.com/784356.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       PruningOfEntriesForDynamicFrames_ChildRemoved) {
  GURL main_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  // Repeatedly create and remove a frame from a script.
  std::string script = R"(
        (async () => {
           for (let i = 0; i < 5; i++) {
             // Create and remove an iframe.
             let iframe = document.createElement('iframe');
             document.body.appendChild(iframe);
             document.body.removeChild(iframe);
             // Let the message loop run (this works in an async function).
             await new Promise(resolve => setTimeout(resolve, 0));
           }
           return 'done-with-test';
        })(); )";
  EXPECT_EQ("done-with-test", EvalJs(shell(), script));

  // Grab the last committed entry.
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(main_url, entry->GetURL());

  // Verify that the number of FrameNavigationEntries stayed low (i.e. that we
  // do not retain history entries for the 5 frames removed by the test).
  EXPECT_EQ(0U, entry->root_node()->children.size());

  // Sanity check - there are no children in the frame tree.
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(0U, root->child_count());
}

// This test helps verify that the browser does not retain history entries
// for removed frames *if* the removed frame was created by a script.
// Such frames get a fresh, random, unique name every time they are created
// or recreated and therefore in such case will never match previous history
// entries.  See also https://crbug.com/784356.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       PruningOfEntriesForDynamicFrames_ParentNavigatedAway) {
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/navigation_controller/page_with_iframe_simple.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // Add 5 dynamic subframes to |frame|.
  RenderFrameHost* frame = ChildFrameAt(current_main_frame_host(), 0);
  std::string script = R"(
        for (var i = 0; i < 5; i++) {
          var iframe = document.createElement("iframe");
          document.body.appendChild(iframe);
        }; )";
  EXPECT_TRUE(ExecJs(frame, script));

  // Verify that now there are 5 FNEs for the dynamic frames.
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(main_url, entry->GetURL());
  EXPECT_EQ(1U, entry->root_node()->children.size());
  EXPECT_EQ(5U, entry->root_node()->children[0]->children.size());

  // Navigate |frame| (the parent of the dynamic frames) away.
  // This will destroy the 5 dynamic children of |frame|.
  GURL next_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
  EXPECT_TRUE(NavigateIframeToURL(shell()->web_contents(), "frame", next_url));

  // Verify that there are now 0 FNEs for the dynamic frames.
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(main_url, entry->GetURL());
  EXPECT_EQ(1U, entry->root_node()->children.size());
  EXPECT_EQ(0U, entry->root_node()->children[0]->children.size());
}

// This test helps verify that the browser does not retain history entries
// for removed frames *if* the removed frame was created by a script.
// Such frames get a fresh, random, unique name every time they are created
// or recreated and therefore in such case will never match previous history
// entries.  See also https://crbug.com/784356.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    PruningOfEntriesForDynamicFrames_MainFrameNavigatedAway) {
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/navigation_controller/page_with_iframe_simple.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // Add 5 dynamic subframes to |frame|.
  RenderFrameHost* frame = ChildFrameAt(current_main_frame_host(), 0);
  std::string script = R"(
        for (var i = 0; i < 5; i++) {
          var iframe = document.createElement("iframe");
          document.body.appendChild(iframe);
        }; )";
  EXPECT_TRUE(ExecJs(frame, script));

  // Verify that now there are 5 FNEs for the dynamic frames.
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(main_url, entry->GetURL());
  EXPECT_EQ(1U, entry->root_node()->children.size());
  EXPECT_EQ(5U, entry->root_node()->children[0]->children.size());

  // Navigate the main frame (the grandparent of the dynamic frames) away.
  // This will destroy the 5 dynamic children of |frame|.
  GURL next_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), next_url));

  // Verify that there are now 0 FNEs for the dynamic frames.
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(main_url, entry->GetURL());
  EXPECT_EQ(1U, entry->root_node()->children.size());
  EXPECT_EQ(0U, entry->root_node()->children[0]->children.size());
}

// This test supplements SpareRenderProcessHostUnitTest to verify that the spare
// RenderProcessHost is actually used in cross-process navigations.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       UtilizationOfSpareRenderProcessHost) {
  GURL first_url = embedded_test_server()->GetURL("a.com", "/title1.html");
  GURL second_url = embedded_test_server()->GetURL("b.com", "/title2.html");
  RenderProcessHost* prev_spare = nullptr;
  RenderProcessHost* curr_spare = nullptr;
  RenderProcessHost* prev_host = nullptr;
  RenderProcessHost* curr_host = nullptr;

  // In the current implementation the spare is not warmed-up until the first
  // real navigation.  It might be okay to change that in the future.
  curr_spare = RenderProcessHostImpl::GetSpareRenderProcessHostForTesting();
  curr_host = shell()->web_contents()->GetPrimaryMainFrame()->GetProcess();
  EXPECT_FALSE(curr_spare);

  // Navigate to the first URL.
  prev_host = curr_host;
  prev_spare = curr_spare;
  EXPECT_TRUE(NavigateToURL(shell(), first_url));
  curr_spare = RenderProcessHostImpl::GetSpareRenderProcessHostForTesting();
  curr_host = shell()->web_contents()->GetPrimaryMainFrame()->GetProcess();
  EXPECT_NE(curr_spare, curr_host);
  // No process swap when navigating away from the initial blank page.
  EXPECT_EQ(prev_host, curr_host);
  // We should always keep a spare RenderProcessHost around in site-per-process
  // mode.  We don't assert what should happen in other scenarios (to give
  // flexibility to platform-specific decisions - e.g. on the desktop there
  // might be no spare outside of site-per-process, but on Android the spare
  // might still be opportunistically warmed up).
  if (AreAllSitesIsolatedForTesting())
    EXPECT_TRUE(curr_spare);

  // Perform a cross-site omnibox navigation.
  prev_host = curr_host;
  prev_spare = curr_spare;

  // With BackForwardCache the old process won't get deleted on navigation as it
  // is still in use by the bfcached document, disable back/forward cache to
  // ensure that the process gets deleted.
  DisableBackForwardCacheForTesting(contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  RenderProcessHostWatcher prev_host_watcher(
      prev_host, RenderProcessHostWatcher::WATCH_FOR_HOST_DESTRUCTION);
  EXPECT_TRUE(NavigateToURL(shell(), second_url));
  // Wait until the |prev_host| goes away - this ensures that the spare will be
  // picked up by subsequent back navigation below.
  prev_host_watcher.Wait();
  curr_spare = RenderProcessHostImpl::GetSpareRenderProcessHostForTesting();
  curr_host = shell()->web_contents()->GetPrimaryMainFrame()->GetProcess();
  // The cross-site omnibox navigation should swap processes.
  EXPECT_NE(prev_host, curr_host);
  // If present, the spare RenderProcessHost should have been be used.
  if (prev_spare)
    EXPECT_EQ(prev_spare, curr_host);
  // A new spare should be warmed-up in site-per-process mode.
  if (AreAllSitesIsolatedForTesting()) {
    EXPECT_TRUE(curr_spare);
    EXPECT_NE(prev_spare, curr_spare);
  }

  // Perform a back navigation.
  prev_host = curr_host;
  prev_spare = curr_spare;
  TestNavigationObserver back_load_observer(shell()->web_contents());
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  controller.GoBack();
  back_load_observer.Wait();
  curr_spare = RenderProcessHostImpl::GetSpareRenderProcessHostForTesting();
  curr_host = shell()->web_contents()->GetPrimaryMainFrame()->GetProcess();
  // The cross-site back navigation should swap processes.
  EXPECT_NE(prev_host, curr_host);
  // If present, the spare RenderProcessHost should have been used.
  if (prev_spare)
    EXPECT_EQ(prev_spare, curr_host);
  // A new spare should be warmed-up in site-per-process mode.
  if (AreAllSitesIsolatedForTesting()) {
    EXPECT_TRUE(curr_spare);
    EXPECT_NE(prev_spare, curr_spare);
  }
}

// Data URLs can have a reference fragment like any other URLs. In this test,
// there are two navigations with the same data URL, but with a different
// reference. The second navigation must be classified as "same-document".
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       DataURLSameDocumentNavigation) {
  GURL url_first("data:text/html,body#foo");
  GURL url_second("data:text/html,body#bar");
  EXPECT_TRUE(url_first.EqualsIgnoringRef(url_second));

  EXPECT_TRUE(NavigateToURL(shell(), url_first));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  FrameNavigateParamsCapturer capturer(root);
  shell()->LoadURL(url_second);
  capturer.Wait();
  EXPECT_TRUE(capturer.is_same_document());
}

// Verify that navigating to a page with status 404 and an empty body will
// result in an error page.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       EmptyBody404CommitsErrorPage) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  GURL url(embedded_test_server()->GetURL("/empty404.html"));

  {
    // Go to a non-existent page resulting in a 404 with an empty body.
    TestNavigationObserverInternal observer(contents());
    shell()->LoadURL(url);
    observer.Wait();

    // The navigation fails and commits a 404 error page.
    EXPECT_FALSE(observer.last_navigation_succeeded());
    EXPECT_EQ(net::ERR_HTTP_RESPONSE_CODE_FAILURE,
              observer.last_net_error_code());
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY,
              observer.last_navigation_type());
    EXPECT_EQ(PAGE_TYPE_ERROR,
              controller.GetLastCommittedEntry()->GetPageType());

    // Check that the error page contains the error code.
    EXPECT_EQ(true,
              EvalJs(contents(), "document.body.innerText.includes('404')"));
  }

  {
    // Reloads will still result in an error page.
    TestNavigationObserverInternal reload_observer(contents());
    shell()->Reload();
    reload_observer.Wait();
    EXPECT_FALSE(reload_observer.last_navigation_succeeded());
    EXPECT_EQ(net::ERR_HTTP_RESPONSE_CODE_FAILURE,
              reload_observer.last_net_error_code());
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              reload_observer.last_navigation_type());
    EXPECT_EQ(PAGE_TYPE_ERROR,
              controller.GetLastCommittedEntry()->GetPageType());
  }

  {
    // Same-URL navigation will still result in an error page.
    TestNavigationObserverInternal same_url_observer(contents());
    controller.LoadURL(url, Referrer(), ui::PAGE_TRANSITION_TYPED,
                       std::string() /* extra_headers */);
    same_url_observer.Wait();
    EXPECT_FALSE(same_url_observer.last_navigation_succeeded());
    EXPECT_EQ(net::ERR_HTTP_RESPONSE_CODE_FAILURE,
              same_url_observer.last_net_error_code());
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY,
              same_url_observer.last_navigation_type());
    EXPECT_EQ(PAGE_TYPE_ERROR,
              controller.GetLastCommittedEntry()->GetPageType());
  }
}

// Verify that navigating to a page with status 500 and an empty body will
// result in an error page.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTestNoServer,
                       EmptyBody500CommitsErrorPage) {
  net::test_server::ControllableHttpResponse response(embedded_test_server(),
                                                      "/title1.html");
  ASSERT_TRUE(embedded_test_server()->Start());
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());

  // Go to a page that has a HTTP 500 status code with an empty body.
  GURL url(embedded_test_server()->GetURL("/title1.html"));
  TestNavigationObserver observer(contents());
  shell()->LoadURL(url);
  response.WaitForRequest();
  response.Send(net::HTTP_INTERNAL_SERVER_ERROR);
  response.Done();
  observer.Wait();

  // The navigation fails and commits a 500 error page.
  EXPECT_FALSE(observer.last_navigation_succeeded());
  EXPECT_EQ(net::ERR_HTTP_RESPONSE_CODE_FAILURE,
            observer.last_net_error_code());
  EXPECT_EQ(PAGE_TYPE_ERROR, controller.GetLastCommittedEntry()->GetPageType());

  // Check that the error page contains the error code.
  EXPECT_EQ(true,
            EvalJs(contents(), "document.body.innerText.includes('500')"));
}

// Verify that navigating to a page with status 404 but a non-empty body won't
// result in an error page.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NonEmpty404BodyDoesNotCommitErrorPage) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());

  // Go to a non-existent page with a non-empty body.
  GURL url(embedded_test_server()->GetURL("/page404.html"));
  TestNavigationObserver observer(contents());
  EXPECT_TRUE(NavigateToURL(shell(), url));
  observer.WaitForNavigationFinished();

  // The navigation succeeds and commits the response body.
  EXPECT_TRUE(observer.last_navigation_succeeded());
  EXPECT_EQ(net::OK, observer.last_net_error_code());
  EXPECT_EQ(PAGE_TYPE_NORMAL,
            controller.GetLastCommittedEntry()->GetPageType());
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ErrorPageNavigationWithoutNavigationRequestGetsKilled) {
  // Navigate normally to a page.
  GURL good_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), good_url));

  // Try to fake an error page navigation by doing a DidCommitProvisionalLoad
  // call. The browser doesn't know about the navigation at all previously.
  GURL bad_url(embedded_test_server()->GetURL("/title2.html"));
  auto params = mojom::DidCommitProvisionalLoadParams::New();
  params->did_create_new_entry = true;
  params->url = bad_url;
  params->referrer = blink::mojom::Referrer::New();
  params->transition = ui::PAGE_TRANSITION_LINK;
  params->page_state = blink::PageState::CreateFromURL(bad_url);
  params->method = "POST";
  params->post_id = 2;
  params->url_is_unreachable = true;
  params->embedding_token = base::UnguessableToken::Create();
  RenderFrameHostImpl* rfh = contents()->GetPrimaryMainFrame();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());
  static_cast<mojom::FrameHost*>(rfh)->DidCommitProvisionalLoad(
      std::move(params),
      mojom::DidCommitProvisionalLoadInterfaceParams::New(
          mojo::PendingRemote<blink::mojom::BrowserInterfaceBroker>()
              .InitWithNewPipeAndPassReceiver()));

  // Verify that the malicious renderer got killed.
  EXPECT_EQ(bad_message::RFH_NO_MATCHING_NAVIGATION_REQUEST_ON_COMMIT,
            kill_waiter.Wait());
}

// Load the same page twice, once as a GET and once as a POST.
// We should update the post state on the NavigationEntry.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       LoadURL_SamePage_DifferentMethod) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Create a form in the page then submit it to create a POST request.
  GURL form_submit_url(embedded_test_server()->GetURL("/title2.html"));
  CreateAndSubmitForm(form_submit_url);

  // Navigate to the page with a "GET" request. This will reload the page with a
  // different method, and the last committed entry should have the POST-related
  // data cleared.
  EXPECT_TRUE(NavigateToURL(shell(), form_submit_url));
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(form_submit_url, entry->GetURL());
  EXPECT_FALSE(entry->GetHasPostData());
  EXPECT_EQ(entry->GetPostID(), -1);
  EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
}

// Similar to LoadURL_SamePage_DifferentMethod but does a renderer-initiated
// navigation instead.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       LoadURL_SamePage_DifferentMethod_RendererInitiated) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Create a form in the page then submit it to create a POST request.
  GURL form_submit_url(embedded_test_server()->GetURL("/title2.html"));
  CreateAndSubmitForm(form_submit_url);

  // Navigate to the page with a "GET" request. This will reload the page with a
  // different method, and the last committed entry should have the POST-related
  // data cleared.
  EXPECT_TRUE(NavigateToURLFromRenderer(shell(), form_submit_url));
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(form_submit_url, entry->GetURL());
  EXPECT_FALSE(entry->GetHasPostData());
  EXPECT_EQ(entry->GetPostID(), -1);
  EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
}

// Tests that doing a form submission that opens a new about:blank tab won't
// crash.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FormSubmissionToNewTab) {
  GURL url_start(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url_start));

  // Create and submit a form that will create a new about:blank tab.
  WebContentsAddedObserver web_contents_added_observer;
  TestNavigationObserver navigation_observer(nullptr, 1);
  navigation_observer.StartWatchingNewWebContents();
  ASSERT_TRUE(ExecuteScript(contents(),
                            R"(let form = document.createElement('form');
                                 form.method = 'POST';
                                 form.target = '_blank';
                                 form.action = 'about:blank';
                                 document.body.appendChild(form);
                                 form.submit();)"));
  WebContentsImpl* popup_contents = static_cast<WebContentsImpl*>(
      web_contents_added_observer.GetWebContents());
  navigation_observer.WaitForNavigationFinished();
  EXPECT_EQ(GURL(url::kAboutBlankURL), popup_contents->GetLastCommittedURL());

  // Ensure that the new tab committed the form submission to about:blank
  // correctly.
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(popup_contents->GetController());
  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(GURL(url::kAboutBlankURL), entry->GetURL());
  EXPECT_TRUE(entry->GetHasPostData());
  EXPECT_NE(entry->GetPostID(), -1);
  EXPECT_EQ("POST", popup_contents->GetPrimaryMainFrame()->last_http_method());
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FormSubmitServerRedirect) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  GURL url2(embedded_test_server()->GetURL("/title2.html"));
  GURL redirect_to_url2_url(
      embedded_test_server()->GetURL("/server-redirect?" + url2.spec()));

  // Create a form in the page then submit it to create a POST request, but the
  // request got server-redirected and lost the POST data.
  TestNavigationObserver form_nav_observer(contents());
  EXPECT_TRUE(
      ExecJs(contents(), JsReplace("var form = document.createElement('form');"
                                   "form.method = 'POST';"
                                   "form.action = $1;"
                                   "document.body.appendChild(form);"
                                   "form.submit();",
                                   redirect_to_url2_url)));
  form_nav_observer.Wait();
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(url2, entry->GetURL());
  EXPECT_FALSE(entry->GetHasPostData());
  EXPECT_EQ(-1, entry->GetPostID());
  EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       FormSubmitClientRedirect) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  GURL redirect_to_url2_url(embedded_test_server()->GetURL(
      "/navigation_controller/client_redirect.html"));

  // Create a form in the page then submit it to create a POST request, but we
  // lost the POST data after client-redirect.
  TestNavigationObserver form_nav_observer(contents());
  EXPECT_TRUE(
      ExecJs(contents(), JsReplace("var form = document.createElement('form');"
                                   "form.method = 'POST';"
                                   "form.action = $1;"
                                   "document.body.appendChild(form);"
                                   "form.submit();",
                                   redirect_to_url2_url)));
  form_nav_observer.Wait();
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(url2, entry->GetURL());
  EXPECT_FALSE(entry->GetHasPostData());
  EXPECT_EQ(-1, entry->GetPostID());
  EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
}

// Counts the occurrences of form repost warning dialogs.
class CountRepostFormWarningWebContentsDelegate : public WebContentsDelegate {
 public:
  CountRepostFormWarningWebContentsDelegate() = default;

  int repost_form_warning_count() { return repost_form_warning_count_; }

  void ShowRepostFormWarningDialog(WebContents* source) override {
    repost_form_warning_count_++;
  }

 private:
  // The number of times ShowRepostFormWarningDialog() was called.
  int repost_form_warning_count_ = 0;
};

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ResetPendingLoadTypeWhenCancelPendingReload) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  auto delegate = std::make_unique<CountRepostFormWarningWebContentsDelegate>();
  contents()->SetDelegate(delegate.get());
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Create a form in the page then submit it to create a POST request.
  GURL form_submit_url(embedded_test_server()->GetURL("/title2.html"));
  const int64_t form_post_id = CreateAndSubmitForm(form_submit_url);
  EXPECT_EQ(0, delegate->repost_form_warning_count());

  // Reload. We should show a repost warning dialog.
  {
    NavigationControllerImpl::ScopedShowRepostDialogForTesting show_repost;
    controller.Reload(ReloadType::NORMAL, true /* check_for_repost */);
    EXPECT_TRUE(WaitForLoadStop(contents()));
    EXPECT_EQ(form_submit_url, contents()->GetLastCommittedURL());
    EXPECT_TRUE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(form_post_id, controller.GetLastCommittedEntry()->GetPostID());
  }
  EXPECT_EQ(ReloadType::NORMAL, controller.pending_reload_);
  EXPECT_EQ(1, delegate->repost_form_warning_count());

  // Trigger a new navigation to cancel pending reload.
  const GURL url1(embedded_test_server()->GetURL("/title3.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  EXPECT_EQ(url1, contents()->GetLastCommittedURL());
  EXPECT_EQ(ReloadType::NONE, controller.pending_reload_);

  // |pending_reload_| has been cleared and no new pending entry will be
  // created.
  controller.ContinuePendingReload();
  EXPECT_FALSE(controller.GetPendingEntry());
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, PostThenReload) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  auto delegate = std::make_unique<CountRepostFormWarningWebContentsDelegate>();
  contents()->SetDelegate(delegate.get());
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Create a form in the page then submit it to create a POST request.
  GURL form_submit_url(embedded_test_server()->GetURL("/title2.html"));
  const int64_t form_post_id = CreateAndSubmitForm(form_submit_url);
  EXPECT_EQ(0, delegate->repost_form_warning_count());

  // Reload. We should show a repost warning dialog.
  {
    NavigationControllerImpl::ScopedShowRepostDialogForTesting show_repost;
    controller.Reload(ReloadType::NORMAL, true /* check_for_repost */);
    EXPECT_TRUE(WaitForLoadStop(contents()));
    EXPECT_EQ(form_submit_url, contents()->GetLastCommittedURL());
    EXPECT_TRUE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(form_post_id, controller.GetLastCommittedEntry()->GetPostID());
  }
  EXPECT_EQ(1, delegate->repost_form_warning_count());
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       PostThenReplaceStateThenReload) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  auto delegate = std::make_unique<CountRepostFormWarningWebContentsDelegate>();
  contents()->SetDelegate(delegate.get());
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Create a form in the page then submit it to create a POST request.
  GURL form_submit_url(embedded_test_server()->GetURL("/title2.html"));
  CreateAndSubmitForm(form_submit_url);
  const int kExpectedRepostFormWarningCount = 0;
  EXPECT_EQ(kExpectedRepostFormWarningCount,
            delegate->repost_form_warning_count());

  // history.replaceState() is called, which clears the POST data.
  GURL replace_url(embedded_test_server()->GetURL("/title2.html#foo"));
  {
    EXPECT_TRUE(
        ExecJs(contents(), "history.replaceState({}, '', 'title2.html#foo')"));
    EXPECT_TRUE(WaitForLoadStop(contents()));
    EXPECT_EQ(replace_url, contents()->GetLastCommittedURL());
    EXPECT_FALSE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(-1, controller.GetLastCommittedEntry()->GetPostID());
  }
  EXPECT_EQ(kExpectedRepostFormWarningCount,
            delegate->repost_form_warning_count());

  // Now reload. replaceState overrides the POST, so we should not show a repost
  // warning dialog.
  {
    NavigationControllerImpl::ScopedShowRepostDialogForTesting show_repost;
    controller.Reload(ReloadType::NORMAL, true /* check_for_repost */);
    EXPECT_TRUE(WaitForLoadStop(contents()));
    EXPECT_EQ(replace_url, contents()->GetLastCommittedURL());
    EXPECT_FALSE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(-1, controller.GetLastCommittedEntry()->GetPostID());
  }
  EXPECT_EQ(kExpectedRepostFormWarningCount,
            delegate->repost_form_warning_count());
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       PostThenPushStateThenReloadThenHistory) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  auto delegate = std::make_unique<CountRepostFormWarningWebContentsDelegate>();
  contents()->SetDelegate(delegate.get());
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Create a form in the page then submit it to create a POST request.
  GURL form_submit_url(embedded_test_server()->GetURL("/title2.html"));
  CreateAndSubmitForm(form_submit_url);
  const int kExpectedRepostFormWarningCount = 0;
  EXPECT_EQ(kExpectedRepostFormWarningCount,
            delegate->repost_form_warning_count());
  EXPECT_EQ("POST", contents()->GetPrimaryMainFrame()->last_http_method());

  // history.pushState() is called, which clears the POST data.
  GURL push_url(embedded_test_server()->GetURL("/title2.html#foo"));
  {
    EXPECT_TRUE(
        ExecJs(contents(), "history.pushState({}, '', 'title2.html#foo')"));
    EXPECT_TRUE(WaitForLoadStop(contents()));
    EXPECT_EQ(push_url, contents()->GetLastCommittedURL());
    EXPECT_FALSE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(-1, controller.GetLastCommittedEntry()->GetPostID());
    EXPECT_EQ(kExpectedRepostFormWarningCount,
              delegate->repost_form_warning_count());
    EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
  }

  // Now reload. pushState overrides the POST, so we should not show a
  // repost warning dialog.
  {
    NavigationControllerImpl::ScopedShowRepostDialogForTesting show_repost;
    controller.Reload(ReloadType::NORMAL, true /* check_for_repost */);
    EXPECT_TRUE(WaitForLoadStop(contents()));
    EXPECT_EQ(push_url, contents()->GetLastCommittedURL());
    EXPECT_FALSE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(-1, controller.GetLastCommittedEntry()->GetPostID());
    EXPECT_EQ(kExpectedRepostFormWarningCount,
              delegate->repost_form_warning_count());
    EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
  }

  // Go back to the first URL. This will be a same document navigation. Even
  // though the original navigation is a "POST" navigation, the POST data is
  // already cleared on the renderer side so it will end as a "GET" navigation.
  {
    NavigationControllerImpl::ScopedShowRepostDialogForTesting show_repost;
    TestNavigationObserver load_observer(shell()->web_contents());
    controller.GoBack();
    load_observer.Wait();
    EXPECT_EQ(form_submit_url, contents()->GetLastCommittedURL());
    EXPECT_FALSE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(-1, controller.GetLastCommittedEntry()->GetPostID());
    EXPECT_EQ(kExpectedRepostFormWarningCount,
              delegate->repost_form_warning_count());
    EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
  }

  // Go forward to the pushState URL. This will be a GET navigation again.
  {
    NavigationControllerImpl::ScopedShowRepostDialogForTesting show_repost;
    TestNavigationObserver load_observer(shell()->web_contents());
    controller.GoForward();
    load_observer.Wait();
    EXPECT_EQ(push_url, contents()->GetLastCommittedURL());
    EXPECT_FALSE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(-1, controller.GetLastCommittedEntry()->GetPostID());
    EXPECT_EQ(kExpectedRepostFormWarningCount,
              delegate->repost_form_warning_count());
    EXPECT_EQ("GET", contents()->GetPrimaryMainFrame()->last_http_method());
  }
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       PostThenFragmentNavigationThenReloadThenHistory) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  auto delegate = std::make_unique<CountRepostFormWarningWebContentsDelegate>();
  contents()->SetDelegate(delegate.get());
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Create a form in the page then submit it to create a POST request.
  GURL form_submit_url(embedded_test_server()->GetURL("/title2.html"));
  const int64_t form_post_id = CreateAndSubmitForm(form_submit_url);
  EXPECT_EQ(0, delegate->repost_form_warning_count());

  // Do a renderer-initiated fragment navigation. This should preserve the POST
  // data.
  GURL fragment_url(embedded_test_server()->GetURL("/title2.html#foo"));
  {
    EXPECT_TRUE(ExecJs(contents(), "location.href = '#foo'"));
    EXPECT_TRUE(WaitForLoadStop(contents()));
    EXPECT_EQ(fragment_url, contents()->GetLastCommittedURL());
    EXPECT_TRUE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(form_post_id, controller.GetLastCommittedEntry()->GetPostID());
    EXPECT_EQ(0, delegate->repost_form_warning_count());
    EXPECT_EQ("POST", contents()->GetPrimaryMainFrame()->last_http_method());
  }

  // Now reload. Fragment navigation keeps the previous POST data, so we should
  // show a repost warning dialog.
  {
    NavigationControllerImpl::ScopedShowRepostDialogForTesting show_repost;
    controller.Reload(ReloadType::NORMAL, true /* check_for_repost */);
    EXPECT_TRUE(WaitForLoadStop(contents()));
    EXPECT_EQ(fragment_url, contents()->GetLastCommittedURL());
    EXPECT_TRUE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(form_post_id, controller.GetLastCommittedEntry()->GetPostID());
    EXPECT_EQ(1, delegate->repost_form_warning_count());
    EXPECT_EQ("POST", contents()->GetPrimaryMainFrame()->last_http_method());
  }

  // Go back. This will be a same document navigation. We won't show a repost
  // warning dialog, but will keep the "POST" method.
  {
    NavigationControllerImpl::ScopedShowRepostDialogForTesting show_repost;
    TestNavigationObserver load_observer(shell()->web_contents());
    controller.GoBack();
    load_observer.Wait();
    EXPECT_EQ(form_submit_url, contents()->GetLastCommittedURL());
    EXPECT_TRUE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(form_post_id, controller.GetLastCommittedEntry()->GetPostID());
    EXPECT_EQ(1, delegate->repost_form_warning_count());
    EXPECT_EQ("POST", contents()->GetPrimaryMainFrame()->last_http_method());
  }

  // Go forward. This will be a same document navigation. We won't show a repost
  // warning dialog, but will keep the "POST" method.
  {
    NavigationControllerImpl::ScopedShowRepostDialogForTesting show_repost;
    TestNavigationObserver load_observer(shell()->web_contents());
    controller.GoForward();
    load_observer.Wait();
    EXPECT_EQ(fragment_url, contents()->GetLastCommittedURL());
    EXPECT_TRUE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(form_post_id, controller.GetLastCommittedEntry()->GetPostID());
    EXPECT_EQ(1, delegate->repost_form_warning_count());
    EXPECT_EQ("POST", contents()->GetPrimaryMainFrame()->last_http_method());
  }
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       PostThenBrowserInitiatedFragmentNavigationThenReload) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  auto delegate = std::make_unique<CountRepostFormWarningWebContentsDelegate>();
  contents()->SetDelegate(delegate.get());
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Create a form in the page then submit it to create a POST request.
  GURL form_submit_url(embedded_test_server()->GetURL("/title2.html"));
  const int64_t form_post_id = CreateAndSubmitForm(form_submit_url);
  EXPECT_EQ(0, delegate->repost_form_warning_count());

  // Do a browser-initiated fragment navigation. This should preserve the POST
  // data.
  GURL fragment_url(embedded_test_server()->GetURL("/title2.html#foo"));
  {
    EXPECT_TRUE(NavigateToURL(shell(), fragment_url));
    EXPECT_EQ(fragment_url, contents()->GetLastCommittedURL());
    EXPECT_TRUE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(form_post_id, controller.GetLastCommittedEntry()->GetPostID());
    EXPECT_EQ(0, delegate->repost_form_warning_count());
    EXPECT_EQ("POST", contents()->GetPrimaryMainFrame()->last_http_method());
  }

  // Now reload. Fragment navigation keeps the previous POST data, so we should
  // show a repost warning dialog.
  {
    NavigationControllerImpl::ScopedShowRepostDialogForTesting show_repost;
    controller.Reload(ReloadType::NORMAL, true /* check_for_repost */);
    EXPECT_TRUE(WaitForLoadStop(contents()));
    EXPECT_EQ(fragment_url, contents()->GetLastCommittedURL());
    EXPECT_TRUE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(form_post_id, controller.GetLastCommittedEntry()->GetPostID());
    EXPECT_EQ(1, delegate->repost_form_warning_count());
    EXPECT_EQ("POST", contents()->GetPrimaryMainFrame()->last_http_method());
  }
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       PostThenJavaScriptURLThenBrowserInitiatedFragment) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  const int64_t start_dsn = controller.GetLastCommittedEntry()
                                ->GetFrameEntry(root)
                                ->document_sequence_number();

  // Create a form in the page then submit it to create a POST request.
  GURL form_submit_url(embedded_test_server()->GetURL("/title2.html"));
  const int64_t form_post_id = CreateAndSubmitForm(form_submit_url);
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ("POST", contents()->GetPrimaryMainFrame()->last_http_method());
  EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
  const int64_t form_dsn = controller.GetLastCommittedEntry()
                               ->GetFrameEntry(root)
                               ->document_sequence_number();
  DCHECK_NE(start_dsn, form_dsn);

  // Do a javascript: URL "navigation", which will create a new document but
  // won't send anything to the browser.
  {
    EXPECT_TRUE(ExecJs(root, R"(window.location = 'javascript:"foo"';)"));
    EXPECT_TRUE(WaitForLoadStop(contents()));

    EXPECT_EQ(form_submit_url, root->current_url());
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ("foo", EvalJs(shell(), "document.body.innerHTML"));
    EXPECT_TRUE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(form_post_id, controller.GetLastCommittedEntry()->GetPostID());
    EXPECT_EQ("POST", contents()->GetPrimaryMainFrame()->last_http_method());
    EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
    DCHECK_EQ(form_dsn, controller.GetLastCommittedEntry()
                            ->GetFrameEntry(root)
                            ->document_sequence_number());
  }

  // Do a browser-initiated fragment navigation. This should preserve the POST
  // data.
  GURL fragment_url(embedded_test_server()->GetURL("/title2.html#foo"));
  {
    EXPECT_TRUE(NavigateToURL(shell(), fragment_url));
    EXPECT_EQ(fragment_url, contents()->GetLastCommittedURL());

    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_TRUE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(form_post_id, controller.GetLastCommittedEntry()->GetPostID());
    EXPECT_EQ("POST", contents()->GetPrimaryMainFrame()->last_http_method());
    EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());

    DCHECK_EQ(form_dsn, controller.GetLastCommittedEntry()
                            ->GetFrameEntry(root)
                            ->document_sequence_number());
  }
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       PostThenJavaScriptURLThenRendererInitiatedFragment) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  const int64_t start_dsn = controller.GetLastCommittedEntry()
                                ->GetFrameEntry(root)
                                ->document_sequence_number();

  // Create a form in the page then submit it to create a POST request.
  GURL form_submit_url(embedded_test_server()->GetURL("/title2.html"));
  const int64_t form_post_id = CreateAndSubmitForm(form_submit_url);
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ("POST", contents()->GetPrimaryMainFrame()->last_http_method());
  EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
  const int64_t form_dsn = controller.GetLastCommittedEntry()
                               ->GetFrameEntry(root)
                               ->document_sequence_number();
  DCHECK_NE(start_dsn, form_dsn);

  // Do a javascript: URL "navigation", which will create a new document but
  // won't send anything to the browser.
  {
    EXPECT_TRUE(ExecJs(root, R"(window.location = 'javascript:"foo"';)"));
    EXPECT_TRUE(WaitForLoadStop(contents()));

    EXPECT_EQ(form_submit_url, root->current_url());
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ("foo", EvalJs(shell(), "document.body.innerHTML"));
    EXPECT_EQ("POST", contents()->GetPrimaryMainFrame()->last_http_method());
    EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
    DCHECK_EQ(form_dsn, controller.GetLastCommittedEntry()
                            ->GetFrameEntry(root)
                            ->document_sequence_number());
  }

  // Do a renderer-initiated fragment navigation. This should preserve the POST
  // data.
  GURL fragment_url(embedded_test_server()->GetURL("/title2.html#foo"));
  {
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "location.href='#foo';"));
    capturer.Wait();
    EXPECT_NE(start_url, root->current_url());
    EXPECT_TRUE(capturer.is_same_document());

    EXPECT_EQ(3, controller.GetEntryCount());
    EXPECT_TRUE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(form_post_id, controller.GetLastCommittedEntry()->GetPostID());
    EXPECT_EQ("POST", contents()->GetPrimaryMainFrame()->last_http_method());
    EXPECT_EQ(200, contents()->GetPrimaryMainFrame()->last_http_status_code());
    DCHECK_EQ(form_dsn, controller.GetLastCommittedEntry()
                            ->GetFrameEntry(root)
                            ->document_sequence_number());
  }
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, PostSubframe) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  auto delegate = std::make_unique<CountRepostFormWarningWebContentsDelegate>();
  contents()->SetDelegate(delegate.get());
  // 1) Load a page with an iframe.
  GURL start_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe_simple.html"));
  GURL iframe_start_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  FrameTreeNode* iframe = root->child_at(0);
  EXPECT_EQ(iframe_start_url,
            iframe->current_frame_host()->GetLastCommittedURL());

  // 2) Create a form in the main frame which submits to the iframe to the same
  // URL with an anchor link, but it will be a cross-document navigation due to
  // the POST data.
  GURL form_submit_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html#foo"));
  {
    FrameNavigateParamsCapturer capturer(root->child_at(0));
    EXPECT_TRUE(
        ExecJs(root, JsReplace(R"(var form = document.createElement('form');
                               form.method = 'POST';
                               form.action = $1;
                               form.target = 'simple_iframe';
                               document.body.appendChild(form);
                               form.submit();)",
                               form_submit_url)));
    capturer.Wait();
    EXPECT_EQ(form_submit_url,
              iframe->current_frame_host()->GetLastCommittedURL());
    EXPECT_EQ(0, delegate->repost_form_warning_count());
    EXPECT_EQ("POST", iframe->current_frame_host()->last_http_method());

    // The last committed entry refers to the main frame, so no POST data there.
    EXPECT_EQ("GET", root->current_frame_host()->last_http_method());
    EXPECT_FALSE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(-1, controller.GetLastCommittedEntry()->GetPostID());
  }
  const int64_t form_post_id = iframe->current_frame_host()->last_post_id();

  // 3) Do a same-document navigation in the iframe. POST ID on the iframe
  // should be retained.
  GURL fragment_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html#bar"));
  {
    NavigationControllerImpl::ScopedShowRepostDialogForTesting show_repost;
    EXPECT_TRUE(ExecJs(iframe, "location.href = '#bar'"));
    EXPECT_TRUE(WaitForLoadStop(contents()));

    EXPECT_EQ(fragment_url,
              iframe->current_frame_host()->GetLastCommittedURL());
    EXPECT_EQ(0, delegate->repost_form_warning_count());
    EXPECT_EQ("POST", iframe->current_frame_host()->last_http_method());
    EXPECT_EQ(form_post_id, iframe->current_frame_host()->last_post_id());

    // The last committed entry refers to the main frame, so no POST data there.
    EXPECT_EQ("GET", root->current_frame_host()->last_http_method());
    EXPECT_FALSE(controller.GetLastCommittedEntry()->GetHasPostData());
    EXPECT_EQ(-1, controller.GetLastCommittedEntry()->GetPostID());
  }

  // 4) Reload the iframe. The iframe will keep the POST ID.
  {
    NavigationControllerImpl::ScopedShowRepostDialogForTesting show_repost;
    FrameNavigateParamsCapturer capturer(iframe);
    EXPECT_TRUE(ExecJs(iframe, "location.reload();"));
    capturer.Wait();
    EXPECT_EQ(fragment_url,
              iframe->current_frame_host()->GetLastCommittedURL());
    EXPECT_EQ(0, delegate->repost_form_warning_count());
    EXPECT_EQ("POST", iframe->current_frame_host()->last_http_method());
    EXPECT_EQ(form_post_id, iframe->current_frame_host()->last_post_id());
  }

  // 5) Reload the tab. The iframe will reload the original page it loaded, and
  // the POST ID on the iframe will be removed.
  {
    NavigationControllerImpl::ScopedShowRepostDialogForTesting show_repost;
    controller.Reload(ReloadType::NORMAL, true /* check_for_repost */);
    EXPECT_TRUE(WaitForLoadStop(contents()));
    iframe = root->child_at(0);
    EXPECT_EQ(iframe_start_url,
              iframe->current_frame_host()->GetLastCommittedURL());
    EXPECT_EQ(0, delegate->repost_form_warning_count());
    EXPECT_EQ("GET", iframe->current_frame_host()->last_http_method());
    EXPECT_EQ(-1, iframe->current_frame_host()->last_post_id());
  }
}

// Verify that a session history navigation which results in a different
// SiteInstance from the original commit is correctly handled - classified
// as new navigation with replacement, resulting in no new navigation
// entries.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SiteInstanceChangeOnHistoryNavigation) {
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url2(embedded_test_server()->GetURL("b.com", "/title2.html"));
  GURL url3(embedded_test_server()->GetURL("c.com", "/title3.html"));
  GURL redirecting_url(embedded_test_server()->GetURL(
      "a.com", "/server-redirect?" + url3.spec()));

  // Start with an initial URL.
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(url1, controller.GetEntryAtIndex(0)->GetURL());
  scoped_refptr<SiteInstance> initial_site_instance =
      root->current_frame_host()->GetSiteInstance();

  {
    // history.replaceState(), pointing to a URL that would redirect to |url3|.
    FrameNavigateParamsCapturer capturer(root);
    std::string script =
        "history.replaceState({}, '', '" + redirecting_url.spec() + "')";
    EXPECT_TRUE(ExecJs(root, script));
    capturer.Wait();
  }
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  EXPECT_EQ(redirecting_url, controller.GetEntryAtIndex(0)->GetURL());
  EXPECT_EQ(initial_site_instance,
            root->current_frame_host()->GetSiteInstance());

  // Navigate to a new URL to get new session history entry.
  EXPECT_TRUE(NavigateToURL(shell(), url2));
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
  EXPECT_NE(initial_site_instance,
            root->current_frame_host()->GetSiteInstance());

  // This test expects using different SiteInstance/URL when navigating back.
  // This won't happen with BackForwardCache as document is restored directly
  // instead of redirecting, disable back/forward cache to ensure that redirect
  // happens on history navigation.
  DisableBackForwardCacheForTesting(contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  // Back, which should redirect to |url3|.
  FrameNavigateParamsCapturer capturer(root);
  shell()->web_contents()->GetController().GoBack();
  capturer.Wait();
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());

  NavigationEntry* entry = controller.GetEntryAtIndex(0);
  EXPECT_EQ(entry, controller.GetLastCommittedEntry());
  EXPECT_EQ(url3, entry->GetURL());
  if (AreAllSitesIsolatedForTesting()) {
    EXPECT_NE(initial_site_instance,
              root->current_frame_host()->GetSiteInstance());
    EXPECT_EQ(
        SiteInfo::CreateForTesting(
            IsolationContext(shell()->web_contents()->GetBrowserContext()),
            url3),
        root->current_frame_host()->GetSiteInstance()->GetSiteInfo());
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
  } else {
    EXPECT_EQ(initial_site_instance,
              root->current_frame_host()->GetSiteInstance());
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
  }
}

// Tests that user activation/gesture is not "inherited" by a new document
// on cross-site browser-initiated navigation.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    NoGestureInheritanceAfterCrossSiteNavigation_BrowserInitiated) {
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url2(embedded_test_server()->GetURL("b.com", "/title2.html"));
  GURL url3(embedded_test_server()->GetURL("c.com", "/title3.html"));
  // Initial navigation.
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  {
    // Renderer-initiated navigation with user gesture.
    FrameNavigateParamsCapturer url2_capturer(root);
    EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url2));
    url2_capturer.Wait();
    EXPECT_TRUE(url2_capturer.has_user_gesture());
    EXPECT_TRUE(root->current_frame_host()
                    ->last_committed_common_params_has_user_gesture());
  }

  {
    // Cross-site browser-initiated navigation without user gesture.
    FrameNavigateParamsCapturer url3_capturer(root);
    EXPECT_TRUE(NavigateToURL(shell(), url3));
    url3_capturer.Wait();
    EXPECT_FALSE(url3_capturer.has_user_gesture());
    EXPECT_FALSE(root->current_frame_host()
                     ->last_committed_common_params_has_user_gesture());
  }
}

// Tests that user activation/gesture is not "inherited" by a new document
// on cross-site renderer-initiated navigation.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    NoGestureInheritanceAfterCrossSiteNavigation_RendererInitiated) {
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url2(embedded_test_server()->GetURL("b.com", "/title2.html"));
  GURL url3(embedded_test_server()->GetURL("c.com", "/title3.html"));
  // Initial navigation.
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  {
    // Renderer-initiated navigation with user gesture.
    FrameNavigateParamsCapturer url2_capturer(root);
    EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url2));
    url2_capturer.Wait();
    EXPECT_TRUE(url2_capturer.has_user_gesture());
    EXPECT_TRUE(root->current_frame_host()
                    ->last_committed_common_params_has_user_gesture());
  }

  {
    // Cross-site renderer-initiated navigation without user gesture.
    FrameNavigateParamsCapturer url3_capturer(root);
    EXPECT_TRUE(NavigateToURLFromRendererWithoutUserGesture(shell(), url3));
    url3_capturer.Wait();
    EXPECT_FALSE(url3_capturer.has_user_gesture());
    EXPECT_FALSE(root->current_frame_host()
                     ->last_committed_common_params_has_user_gesture());
  }
}

// Tests that user activation/gesture is not "inherited" by a new document
// on same-site browser-initiated navigation.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    NoGestureInheritanceAfterSameSiteNavigation_BrowserInitiated) {
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  GURL url3(embedded_test_server()->GetURL("a.com", "/title3.html"));
  // Initial navigation.
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  {
    // Renderer-initiated navigation with user gesture.
    FrameNavigateParamsCapturer url2_capturer(root);
    EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url2));
    url2_capturer.Wait();
    EXPECT_TRUE(url2_capturer.has_user_gesture());
    EXPECT_TRUE(root->current_frame_host()
                    ->last_committed_common_params_has_user_gesture());
  }

  {
    // Same-site browser-initiated navigation without user gesture.
    FrameNavigateParamsCapturer url3_capturer(root);
    EXPECT_TRUE(NavigateToURL(shell(), url3));
    url3_capturer.Wait();
    EXPECT_FALSE(url3_capturer.has_user_gesture());
    EXPECT_FALSE(root->current_frame_host()
                     ->last_committed_common_params_has_user_gesture());
  }
}

// Tests that user activation/gesture is not "inherited" by a new document
// on same-site renderer-initiated navigation.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    NoGestureInheritanceAfterSameSiteNavigation_RendererInitiated) {
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  GURL url3(embedded_test_server()->GetURL("a.com", "/title3.html"));
  // Initial navigation.
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  {
    // Renderer-initiated navigation with user gesture.
    FrameNavigateParamsCapturer url2_capturer(root);
    EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url2));
    url2_capturer.Wait();
    EXPECT_TRUE(url2_capturer.has_user_gesture());
    EXPECT_TRUE(root->current_frame_host()
                    ->last_committed_common_params_has_user_gesture());
  }

  {
    // Same-site renderer-initiated navigation without user gesture.
    FrameNavigateParamsCapturer url3_capturer(root);
    EXPECT_TRUE(NavigateToURLFromRendererWithoutUserGesture(shell(), url3));
    url3_capturer.Wait();
    EXPECT_FALSE(url3_capturer.has_user_gesture());
    EXPECT_FALSE(root->current_frame_host()
                     ->last_committed_common_params_has_user_gesture());
  }
}

// Tests that user activation/gesture returned by DidCommitProvisionalLoadParams
// reflects the latest navigation's gesture on a document when the initial
// document load was loaded with no user activation/gesture.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    GestureChangesAfterSameDocumentNavOnDocumentLoadedWithoutGesture) {
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url2(embedded_test_server()->GetURL("a.com", "/title1.html#a"));
  GURL url3(embedded_test_server()->GetURL("a.com", "/title1.html#b"));
  GURL url4(embedded_test_server()->GetURL("a.com", "/title1.html#c"));
  GURL url5(embedded_test_server()->GetURL("a.com", "/title1.html#d"));
  WebContentsImpl* contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = contents->GetPrimaryFrameTree().root();

  {
    // Initial navigation without user gesture.
    FrameNavigateParamsCapturer url1_capturer(root);
    EXPECT_TRUE(NavigateToURL(shell(), url1));
    url1_capturer.Wait();
    EXPECT_FALSE(url1_capturer.has_user_gesture());
    EXPECT_FALSE(root->current_frame_host()
                     ->last_committed_common_params_has_user_gesture());
  }

  {
    // Renderer-initiated same-document navigation without user gesture.
    FrameNavigateParamsCapturer url2_capturer(root);
    EXPECT_TRUE(NavigateToURLFromRendererWithoutUserGesture(shell(), url2));
    url2_capturer.Wait();
    EXPECT_FALSE(url2_capturer.has_user_gesture());
    EXPECT_FALSE(root->current_frame_host()
                     ->last_committed_common_params_has_user_gesture());
  }

  {
    // Renderer-initiated same-document navigation with user gesture.
    FrameNavigateParamsCapturer url3_capturer(root);
    EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url3));
    url3_capturer.Wait();
    EXPECT_TRUE(url3_capturer.has_user_gesture());
    EXPECT_TRUE(root->current_frame_host()
                    ->last_committed_common_params_has_user_gesture());
  }

  {
    // Browser-initiated same-document navigation without user gesture.
    FrameNavigateParamsCapturer url4_capturer(root);
    EXPECT_TRUE(NavigateToURL(shell(), url4));
    url4_capturer.Wait();
    EXPECT_FALSE(url4_capturer.has_user_gesture());
    EXPECT_FALSE(root->current_frame_host()
                     ->last_committed_common_params_has_user_gesture());
  }

  {
    // Browser-initiated same-document navigation with user gesture.
    FrameNavigateParamsCapturer url5_capturer(root);
    TestNavigationObserver navigation_observer(contents);
    NavigationController::LoadURLParams params(url5);
    params.transition_type = ui::PageTransitionFromInt(
        ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
    params.has_user_gesture = true;
    contents->GetController().LoadURLWithParams(params);
    navigation_observer.Wait();
    EXPECT_TRUE(navigation_observer.last_navigation_succeeded());
    url5_capturer.Wait();
    EXPECT_TRUE(url5_capturer.has_user_gesture());
    EXPECT_TRUE(root->current_frame_host()
                    ->last_committed_common_params_has_user_gesture());
  }
}

// Tests that user activation/gesture returned by DidCommitProvisionalLoadParams
// reflects the latest navigation's gesture when the initial document load was
// loaded with user activation/gesture.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    GestureChangesAfterSameDocumentNavOnDocumentLoadedWithGesture) {
  // Initial navigation to allow doing navigation from the renderer after this.
  GURL url0(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url0));

  WebContentsImpl* contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  GURL url1(embedded_test_server()->GetURL("a.com", "/title2.html"));
  GURL url2(embedded_test_server()->GetURL("a.com", "/title2.html#foo"));
  GURL url3(embedded_test_server()->GetURL("a.com", "/title2.html#bar"));
  GURL url4(embedded_test_server()->GetURL("a.com", "/title2.html#baz"));

  {
    // Navigation with user gesture.
    FrameNavigateParamsCapturer url1_capturer(root);
    EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url1));
    url1_capturer.Wait();
    EXPECT_TRUE(url1_capturer.has_user_gesture());
    EXPECT_TRUE(root->current_frame_host()
                    ->last_committed_common_params_has_user_gesture());
  }

  {
    // Renderer-initiated same-document navigation without user gesture.
    FrameNavigateParamsCapturer url2_capturer(root);
    EXPECT_TRUE(NavigateToURLFromRendererWithoutUserGesture(shell(), url2));
    url2_capturer.Wait();
    EXPECT_FALSE(url2_capturer.has_user_gesture());
    EXPECT_FALSE(root->current_frame_host()
                     ->last_committed_common_params_has_user_gesture());
  }

  {
    // Browser-initiated same-document navigation without user gesture.
    FrameNavigateParamsCapturer url3_capturer(root);
    EXPECT_TRUE(NavigateToURL(shell(), url3));
    url3_capturer.Wait();
    EXPECT_FALSE(url3_capturer.has_user_gesture());
    EXPECT_FALSE(root->current_frame_host()
                     ->last_committed_common_params_has_user_gesture());
  }

  {
    // Browser-initiated same-document navigation with user gesture.
    FrameNavigateParamsCapturer url4_capturer(root);
    TestNavigationObserver navigation_observer(contents);
    NavigationController::LoadURLParams params(url4);
    params.transition_type = ui::PageTransitionFromInt(
        ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
    params.has_user_gesture = true;
    contents->GetController().LoadURLWithParams(params);
    navigation_observer.Wait();
    EXPECT_TRUE(navigation_observer.last_navigation_succeeded());
    url4_capturer.Wait();
    EXPECT_TRUE(url4_capturer.has_user_gesture());
    EXPECT_TRUE(root->current_frame_host()
                    ->last_committed_common_params_has_user_gesture());
  }
}

// history.back() called twice in the renderer process should not make the user
// navigate back twice.
// Regression test for https://crbug.com/869710
// TODO(crbug.com/1328912): flaky.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    DISABLED_HistoryBackTwiceFromRendererWithoutUserGesture) {
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url2(embedded_test_server()->GetURL("b.com", "/title2.html"));
  GURL url3(embedded_test_server()->GetURL("c.com", "/title3.html"));

  EXPECT_TRUE(NavigateToURL(shell(), url1));
  EXPECT_TRUE(NavigateToURL(shell(), url2));
  EXPECT_TRUE(NavigateToURL(shell(), url3));

  EXPECT_TRUE(ExecJs(shell(), "history.back(); history.back();",
                     EXECUTE_SCRIPT_NO_USER_GESTURE));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  EXPECT_EQ(url2, shell()->web_contents()->GetLastCommittedURL());
}

// history.back() called twice in the renderer process should not make the user
// navigate back twice. Even with a user gesture.
// Regression test for https://crbug.com/869710
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       HistoryBackTwiceFromRendererWithUserGesture) {
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url2(embedded_test_server()->GetURL("b.com", "/title2.html"));
  GURL url3(embedded_test_server()->GetURL("c.com", "/title3.html"));

  EXPECT_TRUE(NavigateToURL(shell(), url1));
  EXPECT_TRUE(NavigateToURL(shell(), url2));
  EXPECT_TRUE(NavigateToURL(shell(), url3));

  EXPECT_TRUE(ExecJs(shell(), "history.back(); history.back();"));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  // TODO(https://crbug.com/869710): This should be url2.
  EXPECT_EQ(url1, shell()->web_contents()->GetLastCommittedURL());
}

// Test to verify that LoadPostCommitErrorPage loads an error page even with a
// valid URL.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       BrowserInitiatedLoadPostCommitErrorPage) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  GURL url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url));

  RenderFrameHost* root = shell()->web_contents()->GetPrimaryMainFrame();
  scoped_refptr<SiteInstance> success_site_instance = root->GetSiteInstance();

  std::string error_html = "Error page";
  TestNavigationObserver error_observer(shell()->web_contents());
  controller.LoadPostCommitErrorPage(root, url, error_html,
                                     net::ERR_BLOCKED_BY_CLIENT);
  error_observer.Wait();

  scoped_refptr<SiteInstance> error_site_instance =
      shell()->web_contents()->GetPrimaryMainFrame()->GetSiteInstance();

  EXPECT_FALSE(error_observer.last_navigation_succeeded());
  EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, error_observer.last_net_error_code());
  EXPECT_EQ(PAGE_TYPE_ERROR, controller.GetLastCommittedEntry()->GetPageType());
  EXPECT_EQ(error_html, EvalJs(shell(), "document.body.innerHTML"));

  if (!SiteIsolationPolicy::IsErrorPageIsolationEnabled(true))
    return;

  // Verify the error page committed to the error page process.
  EXPECT_NE(success_site_instance, error_site_instance);
  EXPECT_TRUE(
      success_site_instance->IsRelatedSiteInstance(error_site_instance.get()));
  EXPECT_NE(success_site_instance->GetProcess()->GetID(),
            error_site_instance->GetProcess()->GetID());
  EXPECT_EQ(GURL(kUnreachableWebDataURL), error_site_instance->GetSiteURL());

  EXPECT_TRUE(
      error_site_instance->GetProcess()->GetProcessLock().is_error_page());
}

// Test to verify that LoadPostCommitErrorPage loads an error page in a subframe
// correctly.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       BrowserInitiatedLoadPostCommitErrorPageForSubframe) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  GURL url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe.html"));

  EXPECT_TRUE(NavigateToURL(shell(), url));

  RenderFrameHost* child =
      ChildFrameAt(shell()->web_contents()->GetPrimaryMainFrame(), 0);
  scoped_refptr<SiteInstance> success_site_instance = child->GetSiteInstance();

  std::string error_html = "Error page";
  TestNavigationObserver error_observer(shell()->web_contents());
  controller.LoadPostCommitErrorPage(child, url, error_html,
                                     net::ERR_BLOCKED_BY_CLIENT);
  error_observer.Wait();

  // If error page isolation is enabled the `child` pointer will be invalid
  // and should be retrieved again. It is safe to do so even when disabled.
  child = ChildFrameAt(shell()->web_contents()->GetPrimaryMainFrame(), 0);

  EXPECT_FALSE(error_observer.last_navigation_succeeded());
  EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, error_observer.last_net_error_code());
  EXPECT_EQ(child->GetLastCommittedURL(), url);
  EXPECT_EQ(error_html, EvalJs(child, "document.body.innerHTML"));

  // Verify that the subframe error page committed in the correct SiteInstance.
  EXPECT_TRUE(IsExpectedSubframeErrorTransition(success_site_instance.get(),
                                                child->GetSiteInstance()));
}

// Checks that a browser initiated error page navigation in a frame pending
// deletion is ignored and does not result in a crash. See
// https://crbug.com/1019180.
// TODO(crbug.com/1291862): Flaky failures
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_BrowserInitiatedLoadPostCommitErrorPageIgnoredForFramePendingDeletion \
  DISABLED_BrowserInitiatedLoadPostCommitErrorPageIgnoredForFramePendingDeletion
#else
#define MAYBE_BrowserInitiatedLoadPostCommitErrorPageIgnoredForFramePendingDeletion \
  BrowserInitiatedLoadPostCommitErrorPageIgnoredForFramePendingDeletion
#endif
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    MAYBE_BrowserInitiatedLoadPostCommitErrorPageIgnoredForFramePendingDeletion) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  GURL url(embedded_test_server()->GetURL("a.com", "/page_with_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url));

  RenderFrameHost* frame = shell()->web_contents()->GetPrimaryMainFrame();

  // Create an unload handler and force the browser process to wait before
  // deleting |frame|.
  EXPECT_TRUE(ExecJs(frame, R"(
             window.onunload=function(e){
               window.domAutomationController.send('done');
             };)"));

  // With BackForwardCache, old RenderFrameHost won't enter pending deletion
  // on navigation as it is stored in bfcache, disable back/forward cache to
  // ensure that the RFH will enter pending deletion state.
  DisableBackForwardCacheForTesting(contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  // Navigate the main frame cross-process and wait for the unload event to
  // fire.
  DOMMessageQueue dom_message_queue(frame);
  GURL cross_process_url(
      embedded_test_server()->GetURL("b.com", "/page_with_iframe.html"));
  shell()->LoadURL(cross_process_url);

  std::string message;
  EXPECT_TRUE(dom_message_queue.WaitForMessage(&message));
  EXPECT_EQ("\"done\"", message);

  // |frame| is now pending deletion.
  EXPECT_TRUE(static_cast<RenderFrameHostImpl*>(frame)->IsPendingDeletion());

  std::string error_html = "Error page";
  DidStartNavigationObserver did_start_navigation_observer(
      shell()->web_contents());
  controller.LoadPostCommitErrorPage(frame, url, error_html,
                                     net::ERR_BLOCKED_BY_CLIENT);

  // The error page navigation was ignored.
  EXPECT_FALSE(did_start_navigation_observer.observed());
}

// Test to verify that LoadPostCommitErrorPage works correctly when supplied
// with an about:blank url for the error page.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    BrowserInitiatedLoadPostCommitErrorPageWithAboutBlankUrl) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  GURL url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe.html"));

  EXPECT_TRUE(NavigateToURL(shell(), url));

  RenderFrameHost* child =
      ChildFrameAt(shell()->web_contents()->GetPrimaryMainFrame(), 0);
  scoped_refptr<SiteInstance> success_site_instance = child->GetSiteInstance();

  std::string error_html = "Error page";
  GURL error_url("about:blank#error");
  TestNavigationObserver error_observer(shell()->web_contents());
  controller.LoadPostCommitErrorPage(child, error_url, error_html,
                                     net::ERR_BLOCKED_BY_CLIENT);
  error_observer.Wait();

  // If error page isolation is enabled the `child` pointer will be invalid
  // and should be retrieved again. It is safe to do so even when disabled.
  child = ChildFrameAt(shell()->web_contents()->GetPrimaryMainFrame(), 0);

  EXPECT_FALSE(error_observer.last_navigation_succeeded());
  EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, error_observer.last_net_error_code());
  EXPECT_EQ(child->GetLastCommittedURL(), error_url);
  EXPECT_EQ(error_html, EvalJs(child, "document.body.innerHTML"));

  // Verify that the subframe error page committed in the correct SiteInstance.
  // process.
  EXPECT_TRUE(IsExpectedSubframeErrorTransition(success_site_instance.get(),
                                                child->GetSiteInstance()));
}

// Test to verify that LoadPostCommitErrorPage works correctly when done on a
// popup's main frame without any committed entry.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       LoadPostCommitErrorPageFromPopupWithoutCommittedEntry) {
  // Navigate to a page.
  GURL url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url));

  // Open a popup that never finishes loading and never commits any entry.
  GURL hung_url(embedded_test_server()->GetURL("/hung"));
  Shell* popup;
  {
    ShellAddedObserver new_shell_observer;
    EXPECT_TRUE(ExecJs(shell(), "var window = window.open('/hung');"));
    popup = new_shell_observer.GetShell();
  }

  WebContentsImpl* popup_contents =
      static_cast<WebContentsImpl*>(popup->web_contents());
  NavigationControllerImpl& controller = popup_contents->GetController();
  RenderFrameHostImpl* popup_main_frame = popup_contents->GetPrimaryMainFrame();
  scoped_refptr<SiteInstance> original_site_instance =
      popup_main_frame->GetSiteInstance();

  // The last committed URL is the empty URL, as it never committed.
  EXPECT_EQ(GURL(), popup_main_frame->GetLastCommittedURL());
  EXPECT_FALSE(popup_main_frame->has_committed_any_navigation());

  // Call LoadPostCommitErrorPage on the popup.
  std::string error_html = "Error page";
  TestNavigationObserver error_observer(popup_contents);
  controller.LoadPostCommitErrorPage(popup_main_frame,
                                     popup_main_frame->GetLastCommittedURL(),
                                     error_html, net::ERR_BLOCKED_BY_CLIENT);
  error_observer.Wait();

  // The post-commit error page committed an error page and sets the last
  // committed URL to about:blank.
  popup_main_frame = popup_contents->GetPrimaryMainFrame();
  EXPECT_EQ(popup_main_frame->GetLastCommittedURL(), GURL("about:blank"));
  EXPECT_FALSE(error_observer.last_navigation_succeeded());
  EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, error_observer.last_net_error_code());
  EXPECT_EQ(error_html, EvalJs(popup_main_frame, "document.body.innerHTML"));

  // Verify that the error page committed in the error page process.
  scoped_refptr<SiteInstance> error_site_instance =
      popup_main_frame->GetSiteInstance();
  EXPECT_NE(original_site_instance, error_site_instance);
  EXPECT_EQ(GURL(kUnreachableWebDataURL), error_site_instance->GetSiteURL());

  // The URL displayed in the URL bar is about:blank.
  EXPECT_EQ(GURL("about:blank"), popup_contents->GetVisibleURL());
  // The opener frame can still access the popup window.
  EXPECT_EQ(false, EvalJs(shell()->web_contents()->GetPrimaryMainFrame(),
                          "!!(window.closed)"));
}

// Test to verify that LoadPostCommitErrorPage works correctly when done on an
// iframe without any committed entry.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       LoadPostCommitErrorPageFromFrameWithoutCommittedEntry) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  // Navigate to a page.
  GURL url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url));

  // Add an iframe that never finishes loading and never commits any entry.
  GURL hung_url(embedded_test_server()->GetURL("/hung"));
  {
    TestNavigationManager hung_nav(contents(), hung_url);
    EXPECT_TRUE(ExecJs(shell(), R"(
          var iframe = document.createElement('iframe');
          iframe.src = '/hung';
          document.body.appendChild(iframe);
    )"));
    EXPECT_TRUE(hung_nav.WaitForRequestStart());
  }

  RenderFrameHostImpl* child = static_cast<RenderFrameHostImpl*>(
      ChildFrameAt(shell()->web_contents()->GetPrimaryMainFrame(), 0));
  scoped_refptr<SiteInstance> success_site_instance = child->GetSiteInstance();
  // The last committed URL is the empty URL, as it never committed.
  EXPECT_EQ(GURL(), child->GetLastCommittedURL());
  EXPECT_FALSE(child->has_committed_any_navigation());
  // The main frame can initially access the iframe's contentDocument.
  EXPECT_EQ(true, EvalJs(shell()->web_contents()->GetPrimaryMainFrame(),
                         "!!(iframe.contentDocument)"));

  // Call LoadPostCommitErrorPage on the iframe.
  std::string error_html = "Error page";
  TestNavigationObserver error_observer(shell()->web_contents());
  controller.LoadPostCommitErrorPage(child, child->GetLastCommittedURL(),
                                     error_html, net::ERR_BLOCKED_BY_CLIENT);
  error_observer.Wait();

  // The post-commit error page committed an error page and sets the last
  // committed URL to about:blank.
  child = static_cast<RenderFrameHostImpl*>(
      ChildFrameAt(shell()->web_contents()->GetPrimaryMainFrame(), 0));
  EXPECT_EQ(child->GetLastCommittedURL(), GURL("about:blank"));
  EXPECT_FALSE(error_observer.last_navigation_succeeded());
  EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, error_observer.last_net_error_code());
  EXPECT_EQ(error_html, EvalJs(child, "document.body.innerHTML"));

  // Verify that the subframe error page committed in the correct
  // SiteInstance.
  EXPECT_TRUE(IsExpectedSubframeErrorTransition(success_site_instance.get(),
                                                child->GetSiteInstance()));

  // Since the iframe is now showing an error page, the main frame can no longer
  // access its contentDocument.
  EXPECT_EQ(false, EvalJs(shell()->web_contents()->GetPrimaryMainFrame(),
                          "!!(iframe.contentDocument)"));
}

// Similar to LoadPostCommitErrorPageFromFrameWithoutCommittedEntry, but with
// the addition of CSP that will block the iframe from navigating to an empty
// URL (but not about:blank).
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    LoadPostCommitErrorPageFromFrameWithoutCommittedEntryBlockedByCSP) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  // Navigate to a page.
  GURL url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url));

  // Add frame-src CSP via a new <meta> element that only allows same-origin
  // iframe.
  EXPECT_TRUE(ExecJs(shell(),
                     R"(var meta = document.createElement('meta');
             meta.httpEquiv = 'Content-Security-Policy';
             meta.content = "frame-src 'self'";
             document.getElementsByTagName('head')[0].appendChild(meta);)"));

  // Add an iframe that never finishes loading and never commits any entry.
  GURL hung_url(embedded_test_server()->GetURL("/hung"));
  {
    TestNavigationManager hung_nav(contents(), hung_url);
    EXPECT_TRUE(ExecJs(shell(), R"(
          var iframe = document.createElement('iframe');
          iframe.src = '/hung';
          document.body.appendChild(iframe);
    )"));
    EXPECT_TRUE(hung_nav.WaitForRequestStart());
  }

  RenderFrameHostImpl* child = static_cast<RenderFrameHostImpl*>(
      ChildFrameAt(shell()->web_contents()->GetPrimaryMainFrame(), 0));
  scoped_refptr<SiteInstance> success_site_instance = child->GetSiteInstance();
  // The last committed URL is the empty URL, as it never committed.
  EXPECT_EQ(GURL(), child->GetLastCommittedURL());
  EXPECT_FALSE(child->has_committed_any_navigation());
  // The main frame can initially access the iframe's contentDocument.
  EXPECT_EQ(true, EvalJs(shell()->web_contents()->GetPrimaryMainFrame(),
                         "!!(iframe.contentDocument)"));

  // Call LoadPostCommitErrorPage on the iframe.
  std::string error_html = "Error page";
  TestNavigationObserver error_observer(shell()->web_contents());
  controller.LoadPostCommitErrorPage(child, child->GetLastCommittedURL(),
                                     error_html, net::ERR_BLOCKED_BY_CLIENT);
  error_observer.Wait();

  // The post-commit error page committed an error page and sets the last
  // committed URL to about:blank, which is allowed by CSP because it's the same
  // origin as the main frame (because of origin inheritance). So, the net error
  // code is still ERR_BLOCKED_BY_CLIENT instead of ERR_BLOCKED_BY_CSP.
  EXPECT_EQ(child->GetLastCommittedURL(), GURL("about:blank"));
  EXPECT_FALSE(error_observer.last_navigation_succeeded());
  EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, error_observer.last_net_error_code());
  EXPECT_EQ(error_html, EvalJs(child, "document.body.innerHTML"));

  // Verify that the subframe error page committed in the correct
  // SiteInstance.
  EXPECT_TRUE(IsExpectedSubframeErrorTransition(success_site_instance.get(),
                                                child->GetSiteInstance()));

  // Since the iframe is now showing an error page, the main frame can no longer
  // access its contentDocument.
  EXPECT_EQ(false, EvalJs(shell()->web_contents()->GetPrimaryMainFrame(),
                          "!!(iframe.contentDocument)"));
}

// Starts a navigation to |url_to_start_| just before the DidCommitNavigation
// call for |url_to_intercept_| gets processed.
class NavigationStarterBeforeDidCommitNavigation
    : public DidCommitNavigationInterceptor {
 public:
  NavigationStarterBeforeDidCommitNavigation(WebContentsImpl* web_contents,
                                             Shell* shell,
                                             const GURL& url_to_intercept,
                                             const GURL& url_to_start)
      : DidCommitNavigationInterceptor(web_contents),
        shell_(shell),
        url_to_intercept_(url_to_intercept),
        url_to_start_(url_to_start) {}
  ~NavigationStarterBeforeDidCommitNavigation() override = default;

 private:
  // DidCommitNavigationInterceptor:
  bool WillProcessDidCommitNavigation(
      RenderFrameHost* render_frame_host,
      NavigationRequest* navigation_request,
      mojom::DidCommitProvisionalLoadParamsPtr* params,
      mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
      override {
    if ((**params).url == *url_to_intercept_) {
      shell_->LoadURL(*url_to_start_);
    }
    return true;
  }

  raw_ptr<Shell> shell_;
  const raw_ref<const GURL> url_to_intercept_;
  const raw_ref<const GURL> url_to_start_;
};

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       MultipleSameDocumentNavigations) {
  GURL url1(embedded_test_server()->GetURL("/title1.html"));
  GURL url2(embedded_test_server()->GetURL("/title1.html#foo"));
  GURL url3(embedded_test_server()->GetURL("/title1.html#bar"));
  // Navigate to a page.
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  // Do a same-document navigation to |url2|, and start a same-document
  // navigation to |url3| before the DidCommitNavigation message from the |url2|
  // navigation gets processed, causing the NavigationRequest stored for |url2|
  // to be replaced by the NavigationRequest for |url3| if we only allow saving
  // one NavigationRequest for same-document navigations at a time. This will
  // result in the re-creation of the NavigationRequest for |url2|, which might
  // get some important attributes wrong.
  NavigationStarterBeforeDidCommitNavigation url3_navigation_starter(
      contents(), shell(), url2, url3);
  TestNavigationObserver navigations_observer(shell()->web_contents(), 2);
  FrameNavigateParamsCapturer url2_capturer(
      contents()->GetPrimaryFrameTree().root());
  shell()->LoadURL(url2);
  url2_capturer.Wait();
  // The navigation to |url2| must be correctly recognized as a
  // browser-initiated same-document navigation.
  EXPECT_FALSE(url2_capturer.is_renderer_initiated());
  EXPECT_TRUE(url2_capturer.is_same_document());
  navigations_observer.Wait();
  EXPECT_EQ(url3, contents()->GetLastCommittedURL());
}

// Test to verify that after loading a post-commit error page, back is treated
// as navigating to the entry prior to the page that was active when the
// post-commit error page was triggered.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       BackOnBrowserInitiatedErrorPageNavigation) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  GURL url1(embedded_test_server()->GetURL("/title1.html"));
  GURL url2(embedded_test_server()->GetURL("/title2.html"));

  // Navigate to a valid page.
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  int initial_entry_index = controller.GetLastCommittedEntryIndex();

  // Navigate to a different page.
  EXPECT_TRUE(NavigateToURL(shell(), url2));

  // Trigger a post-commit error page navigation.
  TestNavigationObserver error_observer(shell()->web_contents());
  controller.LoadPostCommitErrorPage(
      shell()->web_contents()->GetPrimaryMainFrame(), url2, "Error Page",
      net::ERR_BLOCKED_BY_CLIENT);
  error_observer.Wait();
  EXPECT_EQ(PAGE_TYPE_ERROR, controller.GetLastCommittedEntry()->GetPageType());
  EXPECT_EQ(2, controller.GetEntryCount());

  // Make sure back is treated as going back from the page that was visible when
  // the post-commit error page was loaded.
  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(initial_entry_index, controller.GetLastCommittedEntryIndex());
  // Check that the next forward entry has been replaced with the original visit
  // to the site (i.e. it shouldn't be the error page).
  EXPECT_EQ(PAGE_TYPE_NORMAL, controller.GetEntryAtOffset(1)->GetPageType());
  EXPECT_EQ(url2, controller.GetEntryAtOffset(1)->GetURL());
}

// Test to verify that after loading a post-commit error page, reload
// triggers a navigation to the previous page (the page that was active when
// the navigation to an error was triggered).
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ReloadOnBrowserInitiatedErrorPageNavigation) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  GURL url(embedded_test_server()->GetURL("/title1.html"));

  // Navigate to a valid page.
  EXPECT_TRUE(NavigateToURL(shell(), url));
  int initial_entry_index = controller.GetLastCommittedEntryIndex();
  int initial_entry_id = controller.GetLastCommittedEntry()->GetUniqueID();

  // Trigger a post-commit error page navigation.
  TestNavigationObserver error_observer(shell()->web_contents());
  controller.LoadPostCommitErrorPage(
      shell()->web_contents()->GetPrimaryMainFrame(), url, "Error Page",
      net::ERR_BLOCKED_BY_CLIENT);
  error_observer.Wait();
  EXPECT_EQ(PAGE_TYPE_ERROR, controller.GetLastCommittedEntry()->GetPageType());
  EXPECT_EQ(1, controller.GetEntryCount());

  // Make sure reload triggers a reload of the original page, not the error,
  // and that we get back to the original entry.
  controller.Reload(ReloadType::NORMAL, false);
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(initial_entry_index, controller.GetLastCommittedEntryIndex());

  // We should be in the initial entry and no longer be in an error page.
  EXPECT_EQ(initial_entry_id,
            controller.GetLastCommittedEntry()->GetUniqueID());
  EXPECT_EQ(PAGE_TYPE_NORMAL,
            controller.GetLastCommittedEntry()->GetPageType());

  // The error page entry shouldn't be available as a forward navigation.
  EXPECT_FALSE(controller.CanGoForward());
  EXPECT_EQ(1, controller.GetEntryCount());
}

// Test clone behavior of post-commit error page navigations.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       CloneOnBrowserInitiatedErrorPageNavigation) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  GURL url(embedded_test_server()->GetURL("/title2.html"));

  // Navigate to a valid page.
  EXPECT_TRUE(NavigateToURL(shell(), url));
  int initial_entry_id = controller.GetLastCommittedEntry()->GetUniqueID();
  std::u16string initial_title = controller.GetLastCommittedEntry()->GetTitle();
  // Trigger a post-commit error page navigation.
  TestNavigationObserver error_observer(shell()->web_contents());
  controller.LoadPostCommitErrorPage(
      shell()->web_contents()->GetPrimaryMainFrame(), url, "Error Page",
      net::ERR_BLOCKED_BY_CLIENT);
  error_observer.Wait();
  EXPECT_EQ(PAGE_TYPE_ERROR, controller.GetLastCommittedEntry()->GetPageType());
  EXPECT_EQ(1, controller.GetEntryCount());

  // Clone the tab and load the entry.
  std::unique_ptr<WebContents> new_tab = shell()->web_contents()->Clone();
  WebContentsImpl* new_tab_impl = static_cast<WebContentsImpl*>(new_tab.get());
  NavigationController& new_controller = new_tab_impl->GetController();
  EXPECT_TRUE(new_controller.IsInitialNavigation());
  EXPECT_TRUE(new_controller.NeedsReload());
  // TODO(carlosil): Before we load, the entry on the new controller is a clone
  // of the post commit error page entry. This is mostly ok since after the load
  // we end up in the right page, but causes navigation state to be lost,
  // ideally we should clone the entry replaced by the error page instead.
  EXPECT_EQ(PAGE_TYPE_ERROR,
            new_controller.GetLastCommittedEntry()->GetPageType());
  {
    TestNavigationObserver clone_observer(new_tab.get());
    new_controller.LoadIfNecessary();
    clone_observer.Wait();
  }
  // The entry on the new controller should be a new one.
  EXPECT_NE(initial_entry_id,
            new_controller.GetLastCommittedEntry()->GetUniqueID());
  // The new entry should keep the URL from the initial navigation, which means
  // after the load it should navigate to the initial page, not to the error.
  EXPECT_EQ(url, new_controller.GetLastCommittedEntry()->GetURL());
  EXPECT_EQ(initial_title, new_controller.GetLastCommittedEntry()->GetTitle());
  EXPECT_EQ(PAGE_TYPE_NORMAL,
            new_controller.GetLastCommittedEntry()->GetPageType());

  // Only one entry should exist in the controller of the cloned tab.
  EXPECT_EQ(1, new_controller.GetEntryCount());
}

// Tests that a same document navigation followed by a client redirect
// do not add any more session history entries and going to previous entry
// works.
// It replaces invalidly behaving unit test added for http://crbug.com/40395.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTestNoServer,
                       ClientRedirectAfterSameDocumentNavigation) {
  net::test_server::ControllableHttpResponse response(embedded_test_server(),
                                                      "/foo.html");
  ASSERT_TRUE(embedded_test_server()->Start());
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // Load an initial page, which the test will eventually go back to.
  const GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));
  EXPECT_EQ(start_url, controller.GetLastCommittedEntry()->GetURL());

  const GURL main_url(embedded_test_server()->GetURL("/foo.html"));
  const GURL last_url(embedded_test_server()->GetURL("/title3.html"));

  // Navigate to foo.html which will do a same document navigation and client
  // redirect.
  TestNavigationManager observer(shell()->web_contents(), last_url);
  shell()->LoadURL(main_url);
  response.WaitForRequest();
  response.Send(
      "HTTP/1.1 200 OK\r\n"
      "Content-Type: text/html; charset=utf-8\r\n"
      "\r\n"
      "<html><script>"
      "window.location.replace('#a');"
      "window.location='/title3.html';"
      "</script></html>");
  ASSERT_TRUE(observer.WaitForNavigationFinished());

  EXPECT_EQ(last_url, controller.GetLastCommittedEntry()->GetURL());
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_TRUE(controller.CanGoBack());

  TestNavigationObserver back_load_observer(shell()->web_contents());
  controller.GoBack();
  back_load_observer.Wait();
  EXPECT_EQ(start_url, controller.GetLastCommittedEntry()->GetURL());
}

class SandboxedNavigationControllerBrowserTest
    : public NavigationControllerBrowserTest {
 protected:
  void SetupNavigation() {
    NavigationControllerImpl& controller =
        static_cast<NavigationControllerImpl&>(
            shell()->web_contents()->GetController());
    GURL preload_url(embedded_test_server()->GetURL(
        "/navigation_controller/page_with_links.html"));
    EXPECT_TRUE(NavigateToURL(shell(), preload_url));
    ASSERT_EQ(1, controller.GetEntryCount());

    GURL main_url(embedded_test_server()->GetURL(
        "/navigation_controller/page_with_sandbox_iframe.html"));
    EXPECT_TRUE(NavigateToURL(shell(), main_url));
    ASSERT_EQ(2, controller.GetEntryCount());

    FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                              ->GetPrimaryFrameTree()
                              .root();
    ASSERT_EQ(2U, root->child_count());
    ASSERT_NE(nullptr, root->child_at(0));
    ASSERT_NE(nullptr, root->child_at(1));
    ASSERT_NE(nullptr, root->child_at(1)->child_at(0));

    GURL sub_subframe_url(embedded_test_server()->GetURL(
        "/navigation_controller/simple_page_2.html"));
    // Navigate sibling frame to simple_page_2.
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), sub_subframe_url));
    ASSERT_EQ(3, controller.GetEntryCount());

    // Navigate sandbox frame to simple_page_2.
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(1)->child_at(0),
                                          sub_subframe_url));
    ASSERT_EQ(4, controller.GetEntryCount());

    // Click link inside sandboxed iframe.
    std::string script = "document.getElementById('test_anchor').click()";
    EXPECT_TRUE(ExecJs(root->child_at(1), script));
    ASSERT_EQ(5, controller.GetEntryCount());
    EXPECT_EQ(4, controller.GetCurrentEntryIndex());

    // History should now be:
    // [preload_url, main(simple1, sandbox(simple1)),
    // main(simple2, sandbox(simple1)), main(simple2, sandbox(simple2)),
    // *main(simple2, sandbox#test(simple2))]
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

// Tests navigations which occur from a sandboxed frame are prevented.
IN_PROC_BROWSER_TEST_P(SandboxedNavigationControllerBrowserTest,
                       TopLevelNavigationFromSandboxSource) {
  SetupNavigation();

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  std::string back_script = "history.back();";
  std::string forward_script = "history.forward();";

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  // Navigate sandbox frame back same-document.
  EXPECT_TRUE(ExecJs(root->child_at(1), back_script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(3, controller.GetCurrentEntryIndex());

  // Navigate innermost frame back cross-document.
  EXPECT_TRUE(ExecJs(root->child_at(1), back_script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(2, controller.GetCurrentEntryIndex());

  // Navigate sibling frame back cross-document. It should fail.
  EXPECT_TRUE(ExecJs(root->child_at(1), back_script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(2, controller.GetCurrentEntryIndex());

  // Try it again and it should fail.
  EXPECT_TRUE(ExecJs(root->child_at(1), back_script));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(2, controller.GetCurrentEntryIndex());

  // Do it browser initiated. Make sure histograms don't change.
  ASSERT_TRUE(controller.CanGoBack());
  controller.GoBack();
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));

  // Go forward to reset state, then a mouse back button navigation.
  // Using the mouse back button should be allowed because it is a
  // UA level default action even though it originates from the
  // renderer side. The sandbox policy shouldn't be applied when it
  // doesn't originate from a script.
  controller.GoForward();
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(2, controller.GetCurrentEntryIndex());
  root->child_at(1)
      ->current_frame_host()
      ->GetRenderWidgetHost()
      ->ForwardMouseEvent(blink::WebMouseEvent(
          blink::WebInputEvent::Type::kMouseUp, gfx::PointF(), gfx::PointF(),
          blink::WebPointerProperties::Button::kBack, 0, 0,
          base::TimeTicks::Now()));
  RunUntilInputProcessed(
      root->child_at(1)->current_frame_host()->GetRenderWidgetHost());
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
}

class SandboxedNavigationControllerWithBfcacheBrowserTest
    : public NavigationControllerBrowserTest {
 protected:
  void SetUp() override {
    feature_list_.InitWithFeaturesAndParameters(
        GetDefaultEnabledBackForwardCacheFeaturesForTesting(
            /*ignore_outstanding_network_request=*/false),
        GetDefaultDisabledBackForwardCacheFeaturesForTesting());
    NavigationControllerBrowserTest::SetUp();
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

// Tests navigations which occur from a sandboxed frame are prevented.
IN_PROC_BROWSER_TEST_P(SandboxedNavigationControllerWithBfcacheBrowserTest,
                       BackNavigationToCachedPageNotAllowed) {
  GURL cached_url(embedded_test_server()->GetURL("a.com", "/title1.html"));

  GURL main_url(embedded_test_server()->GetURL(
      "b.com", "/navigation_controller/page_with_sandbox_iframe.html"));

  EXPECT_TRUE(NavigateToURL(shell(), cached_url));
  RenderFrameHostImpl* cached_rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  content::RenderFrameDeletedObserver observer(cached_rfh);

  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  ASSERT_FALSE(observer.deleted());
  EXPECT_TRUE(cached_rfh->IsInBackForwardCache());

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  ASSERT_EQ(2UL, root->child_count());
  FrameTreeNode* sanboxed_iframe = root->child_at(1);

  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
  // Navigation not allowed. It should fail.
  EXPECT_TRUE(ExecJs(sanboxed_iframe, "history.back();"));
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
}

class SandboxedNavigationControllerPopupBrowserTest
    : public NavigationControllerBrowserTest {
 protected:
  void SetupNavigation() {
    EXPECT_EQ(1u, Shell::windows().size());
    NavigationControllerImpl& controller =
        static_cast<NavigationControllerImpl&>(
            shell()->web_contents()->GetController());
    GURL preload_url(embedded_test_server()->GetURL(
        "/navigation_controller/page_with_sandboxed_iframe_popup.html"));
    EXPECT_TRUE(NavigateToURL(shell(), preload_url));
    ASSERT_EQ(1, controller.GetEntryCount());

    FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                              ->GetPrimaryFrameTree()
                              .root();
    ASSERT_EQ(1U, root->child_count());
    ASSERT_NE(nullptr, root->child_at(0));
    ShellAddedObserver new_shell_observer;
    // Click link inside sandboxed iframe, causing popup open.
    std::string script = "document.getElementById('test_link').click()";
    EXPECT_TRUE(ExecJs(root->child_at(0), script));

    popup_shell_ = new_shell_observer.GetShell();
    EXPECT_TRUE(WaitForLoadStop(popup_shell_->web_contents()));
    FrameTreeNode* popup_root =
        static_cast<WebContentsImpl*>(popup_shell_->web_contents())
            ->GetPrimaryFrameTree()
            .root();
    // Click link inside sandboxed popup, causing the frame to have an
    // additional entry in history state.
    std::string popup_script = "document.getElementById('test_anchor').click()";
    EXPECT_TRUE(ExecJs(popup_root, popup_script));
  }

 protected:
  raw_ptr<Shell, DanglingUntriaged> popup_shell_ = nullptr;

 private:
  base::test::ScopedFeatureList feature_list_;
};

// Tests navigations that sandboxed top level frames still
// can navigate.
IN_PROC_BROWSER_TEST_P(SandboxedNavigationControllerPopupBrowserTest,
                       NavigateSelf) {
  SetupNavigation();

  std::string back_script = "history.back();";
  FrameTreeNode* root =
      static_cast<WebContentsImpl*>(popup_shell_->web_contents())
          ->GetPrimaryFrameTree()
          .root();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      popup_shell_->web_contents()->GetController());
  ASSERT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
  // Navigate sandboxed top frame back.
  EXPECT_TRUE(ExecJs(root, back_script));
  EXPECT_TRUE(WaitForLoadStop(popup_shell_->web_contents()));
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());
}

class NavigationControllerMainDocumentSequenceNumberBrowserTest
    : public NavigationControllerBrowserTest,
      public WebContentsObserver {
 protected:
  void SetUpOnMainThread() override {
    NavigationControllerBrowserTest::SetUpOnMainThread();

    WebContentsObserver::Observe(shell()->web_contents());
  }

  void DidFinishNavigation(NavigationHandle* navigation_handle) override {
    main_frame_document_sequence_numbers_.push_back(
        shell()
            ->web_contents()
            ->GetController()
            .GetLastCommittedEntry()
            ->GetMainFrameDocumentSequenceNumber());
  }

  // Document sequence numbers monotonically increase during the entire lifetime
  // of the browser process. Renumber them starting at 1 to make testing easier.
  std::vector<int64_t> GetProcessedMainDocumentSequenceNumbers() {
    std::vector<int64_t> ids = main_frame_document_sequence_numbers_;
    std::sort(ids.begin(), ids.end());

    std::map<int64_t, int64_t> compressor;
    int current_id = 0;
    for (int64_t value : ids) {
      if (compressor.find(value) == compressor.end())
        compressor[value] = ++current_id;
    }

    std::vector<int64_t> result;
    for (int64_t value : main_frame_document_sequence_numbers_)
      result.push_back(compressor[value]);

    return result;
  }

 private:
  std::vector<int64_t> main_frame_document_sequence_numbers_;
};

IN_PROC_BROWSER_TEST_P(
    NavigationControllerMainDocumentSequenceNumberBrowserTest,
    SubframeNavigation) {
  const GURL url1(
      embedded_test_server()->GetURL("/frame_tree/page_with_one_frame.html"));
  const GURL url2(embedded_test_server()->GetURL("/title1.html"));
  const GURL url3(embedded_test_server()->GetURL("/title2.html"));
  const char kChildFrameId[] = "child0";

  EXPECT_TRUE(NavigateToURL(shell(), url1));
  // The navigation entries are:
  // [*url1(subframe)]

  EXPECT_TRUE(
      NavigateIframeToURL(shell()->web_contents(), kChildFrameId, url2));
  // The navigation entries are:
  // [url1(subframe), *url1(url2)]

  EXPECT_TRUE(NavigateToURL(shell(), url3));
  // The navigation entries are:
  // [url1(subframe), url1(url2), *url3]

  {
    // We are waiting for two navigations here: main frame and subframe.
    // However, when back/forward cache is enabled, back navigation to a page
    // with subframes will not trigger a subframe navigation (since the
    // subframe is cached with the page).
    TestNavigationObserver navigation_observer(
        shell()->web_contents(), IsBackForwardCacheEnabled() ? 1 : 2);
    shell()->GoBackOrForward(-1);
    navigation_observer.WaitForNavigationFinished();
  }
  // The navigation entries are:
  // [url1(subframe), *url1(url2), url3]

  // Main document and child document navigation from the first NavigateToURL
  // and the subframe navigation from NavigateIframeToURL are related to the
  // first main document.
  // The second NavigateToURL navigates to a new main document.
  // The back navigation navigates back both main document and a child document
  // and they are related to the first main document (except when same-site
  // back/forward cache is enabled, where we only navigate the main frame, since
  // the subframe is cached and doesn't need reconstruction/navigation).
  if (!IsBackForwardCacheEnabled()) {
    EXPECT_THAT(GetProcessedMainDocumentSequenceNumbers(),
                ElementsAre(1, 1, 1, 2, 1, 1));
  } else {
    EXPECT_THAT(GetProcessedMainDocumentSequenceNumbers(),
                ElementsAre(1, 1, 1, 2, 1));
  }
}

IN_PROC_BROWSER_TEST_P(
    NavigationControllerMainDocumentSequenceNumberBrowserTest,
    SameDocument) {
  const GURL url1(embedded_test_server()->GetURL("/title1.html"));
  const GURL url1_fragment(embedded_test_server()->GetURL("/title1.html#id_1"));
  const GURL url2(embedded_test_server()->GetURL("/title2.html"));

  EXPECT_TRUE(NavigateToURL(shell(), url1));
  // The navigation entries are:
  // [*url1]

  EXPECT_TRUE(NavigateToURL(shell(), url1_fragment));
  // The navigation entries are:
  // [url1, *url1_fragment]

  EXPECT_TRUE(NavigateToURL(shell(), url2));
  // The navigation entries are:
  // [url1, url1_fragment, *url2]

  {
    TestNavigationObserver navigation_observer(shell()->web_contents());
    shell()->GoBackOrForward(-1);
    navigation_observer.WaitForNavigationFinished();
  }
  // The navigation entries are:
  // [url1, *url1_fragment, url2]

  EXPECT_THAT(GetProcessedMainDocumentSequenceNumbers(),
              ElementsAre(1, 1, 2, 1));
}

namespace {

class DidCommitNavigationCanceller : public DidCommitNavigationInterceptor {
  using CallbackScriptRunner = base::OnceCallback<void()>;

 public:
  DidCommitNavigationCanceller(WebContents* web_contents,
                               const GURL& url_to_cancel,
                               CallbackScriptRunner callback)
      : DidCommitNavigationInterceptor(web_contents),
        url_to_cancel_(url_to_cancel) {
    callback_ = std::move(callback);
  }

  bool WillProcessDidCommitNavigation(
      RenderFrameHost* render_frame_host,
      NavigationRequest* navigation_request,
      mojom::DidCommitProvisionalLoadParamsPtr* params,
      mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
      override {
    if (navigation_request->GetURL() == url_to_cancel_) {
      std::move(callback_).Run();
      return false;
    }
    return true;
  }

 private:
  CallbackScriptRunner callback_;
  GURL url_to_cancel_;
};

}  //  namespace

// When running OpenURL to an invalid URL on a frame proxy it should not spoof
// the url by canceling a main frame navigation.
// See https://crbug.com/966914.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       CrossProcessIframeToInvalidURLCancelsRedirectSpoof) {
  // This tests something that can only happened with out of process iframes.
  if (!AreAllSitesIsolatedForTesting())
    return;

  if (ShouldQueueNavigationsWhenPendingCommitRFHExists()) {
    // If navigation queueing is enabled, the first navigation will be stuck at
    // the "pending commit" stage forever, causing the second navigation to be
    // queued indefinitely, and the test will timeout.
    // TODO(https://crbug.com/1220337): Rewrite the test to defer the first
    // navigation instead, so that it won't cause the second navigation to be
    // stuck.
    return;
  }

  const GURL main_frame_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  const GURL main_frame_url_2(embedded_test_server()->GetURL("/title2.html"));

  // Load the initial page, containing a fully scriptable cross-site iframe.
  EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));

  FrameTreeNode* iframe = static_cast<WebContentsImpl*>(shell()->web_contents())
                              ->GetPrimaryFrameTree()
                              .root()
                              ->child_at(0);

  DidCommitNavigationCanceller canceller(
      shell()->web_contents(), main_frame_url_2,
      base::BindLambdaForTesting([iframe]() {
        EXPECT_TRUE(ExecJs(
            iframe, "parent.location.href = 'chrome-untrusted://1234';"));
      }));

  // This navigation will be raced by a navigation started in the iframe.
  // The NavigationRequest for the first navigation will already be in the
  // RenderFrameHost at this point, and the iframe proxy navigation will
  // proceed because we don't have a FrameTreeNode ongoing navigation.
  // So the main navigation will be cancelled first, by the iframe navigation
  // taking precedence, and the iframe navigation will not get passed network
  // because of the invalid url, getting cancelled as well.
  EXPECT_FALSE(NavigateToURL(shell(), main_frame_url_2));

  // Check that no spoof happened.
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  EXPECT_EQ(controller.GetVisibleEntry()->GetVirtualURL(),
            shell()->web_contents()->GetLastCommittedURL());
}

// Tests a renderer aborting the navigation it started, while still waiting on a
// long cross-process subframe beforeunload handler.
// Regression test: https://crbug.com/972154
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    NavigationAbortDuringLongCrossProcessIframeBeforeUnload) {
  // This test relies on the main frame and the iframe to live in different
  // processes. This allows one renderer process to cancel a navigation while
  // the other renderer process is busy executing its beforeunload handler.
  if (!AreAllSitesIsolatedForTesting())
    return;

  const GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  const GURL navigated_url(
      embedded_test_server()->GetURL("a.com", "/title1.html"));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();

  // Have a first page with a cross process iframe.
  // The iframe itself does have a dialog-showing beforeunload handler.
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  std::string script =
      "window.addEventListener('beforeunload', function (event) {"
      "  event.returnValue='blocked'"
      "});";
  EXPECT_TRUE(ExecJs(root->child_at(0), script));

  NavigationHandleObserver abort_observer(web_contents, navigated_url);
  BeforeUnloadBlockingDelegate beforeunload_pauser(web_contents);

  // Navigate to any page, renderer initiated.
  EXPECT_TRUE(ExecJs(shell(), "location.href = 'title1.html'"));

  // The previous navigation is paused while the beforeunloadhandler dialog is
  // shown to the user. In the meantime, the navigation is aborted:
  beforeunload_pauser.Wait();
  EXPECT_TRUE(ExecJs(shell(), "document.write()"));  // Cancel the navigation.

  // Wait for javascript to get processed, and its consequences (aborting the
  // navigation) to finish. To achieve that we simply run script again.
  EXPECT_EQ(42, EvalJs(shell(), "12 + 30").ExtractInt());

  // Verify that the navigation was aborted as expected.
  EXPECT_FALSE(abort_observer.has_committed());
}

namespace {

// A request handler that returns a simple page for requests using |method| to
// /handle-method-only, and closes the socket for any other method.
std::unique_ptr<net::test_server::HttpResponse> HandleMethodOnly(
    net::test_server::HttpMethod method,
    const net::test_server::HttpRequest& request) {
  if (request.relative_url != "/handle-method-only")
    return nullptr;

  if (request.method != method) {
    return std::make_unique<net::test_server::RawHttpResponse>("", "");
  }
  std::unique_ptr<net::test_server::BasicHttpResponse> response =
      std::make_unique<net::test_server::BasicHttpResponse>();
  response->set_content_type("text/html");
  response->set_content("Success!");
  return response;
}

}  // namespace

// Tests that the navigation entry's method is updated to GET when following a
// 301 redirect that encounters an error page. See https://crbug.com/1041597.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTestNoServer,
                       UpdateMethodOn301RedirectError) {
  // HandleMethodOnly serves the final endpoint that the test ends up at. It
  // lets the test distinguish a GET from a POST by serving a response only for
  // POST requests.
  embedded_test_server()->RegisterRequestHandler(
      base::BindRepeating(&HandleMethodOnly, net::test_server::METHOD_POST));
  ASSERT_TRUE(embedded_test_server()->Start());

  GURL start_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  GURL post_only_url = embedded_test_server()->GetURL("/handle-method-only");
  GURL form_action_url(embedded_test_server()->GetURL("/server-redirect-301?" +
                                                      post_only_url.spec()));

  // Inject a form into the page and submit it, to create a POST request to
  // |form_action_url|. This POST request will redirect to |post_only_url|. The
  // request's method should change to GET while following the redirect,
  // resulting in an error page since |post_only_url| closes the connection on
  // GETs.
  TestNavigationObserver form_nav_observer(shell()->web_contents(), 1);
  EXPECT_TRUE(ExecJs(shell()->web_contents(),
                     JsReplace("var form = document.createElement('form');"
                               "form.method = 'POST';"
                               "form.action = $1;"
                               "document.body.appendChild(form);"
                               "form.submit();",
                               form_action_url.spec())));
  form_nav_observer.Wait();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(post_only_url, entry->GetURL());
  EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
  EXPECT_FALSE(entry->GetHasPostData());

  // When the error page is reloaded, the method should still be GET, resulting
  // in an error page. If https://crbug.com/1041597 regresses, the
  // NavigationEntry's method would be POST and this test would fail, seeing a
  // successful response instead of an error page from |post_only_url|.
  TestNavigationObserver reload_observer(shell()->web_contents(), 1);
  // Set |check_for_repost| to false to avoid hanging the test if the method is
  // improperly set to POST.
  controller.Reload(ReloadType::NORMAL, false);
  reload_observer.Wait();
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(post_only_url, entry->GetURL());
  EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
}

// Tests that the navigation entry's method is preserved as POST when following
// a 307 redirect that encounters an error page. This test is similar to the
// above UpdateMethodOn301RedirectError, but reversed: in this test, the method
// should be preserved as POST.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTestNoServer,
                       UpdateMethodOn307RedirectError) {
  // HandleMethodOnly serves the final endpoint that the test ends up at. It
  // lets the test distinguish a GET from a POST by serving a response only for
  // GET requests.
  embedded_test_server()->RegisterRequestHandler(
      base::BindRepeating(&HandleMethodOnly, net::test_server::METHOD_GET));
  ASSERT_TRUE(embedded_test_server()->Start());

  GURL start_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  GURL get_only_url = embedded_test_server()->GetURL("/handle-method-only");
  GURL form_action_url(embedded_test_server()->GetURL("/server-redirect-307?" +
                                                      get_only_url.spec()));

  // Inject a form into the page and submit it, to create a POST request to
  // |form_action_url|. This POST request will redirect to |get_only_url|. The
  // request's method should stay as POST while following the redirect,
  // resulting in an error page since |get_only_url| closes the connection on
  // POSTs.
  TestNavigationObserver form_nav_observer(shell()->web_contents(), 1);
  EXPECT_TRUE(ExecJs(shell()->web_contents(),
                     JsReplace("var form = document.createElement('form');"
                               "form.method = 'POST';"
                               "form.action = $1;"
                               "document.body.appendChild(form);"
                               "form.submit();",
                               form_action_url.spec())));
  form_nav_observer.Wait();
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(get_only_url, entry->GetURL());
  EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
  EXPECT_TRUE(entry->GetHasPostData());

  // When the error page is reloaded, the method should still be POST, resulting
  // in an error page.
  TestNavigationObserver reload_observer(shell()->web_contents(), 1);
  // Set |check_for_repost| to false to avoid hanging the test with the prompt.
  controller.Reload(ReloadType::NORMAL, false);
  reload_observer.Wait();
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(get_only_url, entry->GetURL());
  EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
}

// Test for a navigation that is
// 1) initiated by a cross-site frame
// 2) same-document
// 3) to a http URL with port 0.
//
// The history: before https://crbug.com/1065532 was fixed, this was a browser
// crash; afterwards, but before crbug.com/1136678 was fixed, it led to a
// renderer kill; now, it should just be a failed navigation (assuming port 0 is
// unreachable).
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SameDocumentNavigationToHttpPortZero) {
  // The test server doesn't support port 0 (and, more generally, serving files
  // from a specific port), so we add a URLLoaderInterceptor that will provide
  // a response to our requests to port 0 later on.
  auto interceptor = URLLoaderInterceptor::ServeFilesFromDirectoryAtOrigin(
      "content/test/data", GURL("http://another-site.com:0"));

  GURL page_url(embedded_test_server()->GetURL(
      "foo.com", "/navigation_controller/simple_page_1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), page_url));

  // Inject a HTTP subframe.
  const char kSubframeScriptTemplate[] = R"(
      var iframe = document.createElement('iframe');
      iframe.src = $1;
      document.body.appendChild(iframe);
  )";
  GURL subframe_initial_url =
      embedded_test_server()->GetURL("another-site.com", "/title2.html");
  {
    TestNavigationObserver subframe_injection_observer(shell()->web_contents(),
                                                       1);
    ASSERT_TRUE(ExecJs(
        shell(), JsReplace(kSubframeScriptTemplate, subframe_initial_url)));
    subframe_injection_observer.Wait();
    ASSERT_TRUE(subframe_injection_observer.last_navigation_succeeded());
  }

  // From the main page initiate a navigation of the cross-site subframe to a
  // http URL that has port=0.  Note that this is valid port according to the
  // URL spec (https://url.spec.whatwg.org/#port-state).
  //
  // Before the fix for SchemeHostPort's handling of port=0 the navigation
  // below would produce a browser process CHECK/crash, because port 0 confused
  // url::Origin::Resolve.
  GURL::Replacements replace_port_and_ref;
  replace_port_and_ref.SetPortStr("0");
  replace_port_and_ref.SetRefStr("someRef");
  GURL subframe_ref_url =
      subframe_initial_url.ReplaceComponents(replace_port_and_ref);

  FrameTreeNode* subframe_tree_node =
      static_cast<WebContentsImpl*>(shell()->web_contents())
          ->GetPrimaryFrameTree()
          .root()
          ->child_at(0);
  // Shouldn't crash.
  ASSERT_TRUE(NavigateToURLFromRenderer(subframe_tree_node, subframe_ref_url));

  // As a reasonableness check, make sure we committed the right URL:
  EXPECT_EQ(subframe_tree_node->current_frame_host()->GetLastCommittedURL(),
            GURL("http://another-site.com:0/title2.html#someRef"));
  EXPECT_EQ(subframe_tree_node->current_frame_host()->GetLastCommittedOrigin(),
            url::Origin::Create(GURL("http://another-site.com:0")));
}

// Navigating a subframe to the same URL should not append a new history entry.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NoHistoryOnNavigationToSameUrl) {
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  // 1) Test navigating a same-site subframe to the same URL.
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL(
                   "a.com", "/cross_site_iframe_factory.html?a(a)")));

  FrameTreeNode* child = contents()->GetPrimaryFrameTree().root()->child_at(0);
  GURL child_url = child->current_url();
  NavigationEntryImpl* previous_entry = controller.GetLastCommittedEntry();
  scoped_refptr<FrameNavigationEntry> previous_frame_entry =
      previous_entry->GetFrameEntry(child);
  EXPECT_EQ(1, controller.GetEntryCount());

  {
    // Navigate the subframe (renderer-initiated) to the same URL it's currently
    // on.
    FrameNavigateParamsCapturer capturer(child);
    ASSERT_TRUE(NavigateToURLFromRenderer(child, child_url));
    capturer.Wait();

    // We reused the previous NavigationEntry and FNE, but replaced the entry in
    // the renderer.
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(previous_frame_entry,
              controller.GetLastCommittedEntry()->GetFrameEntry(child));
    EXPECT_TRUE(capturer.did_replace_entry());
  }

  // 2) Test navigating a cross-site subframe to the same URL.
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL(
                   "a.com", "/cross_site_iframe_factory.html?a(b)")));
  EXPECT_EQ(2, controller.GetEntryCount());

  child = contents()->GetPrimaryFrameTree().root()->child_at(0);
  child_url = child->current_url();

  {
    // Replace history.state to "foo".
    ReplaceState(child, "foo");
    EXPECT_EQ("foo", EvalJs(child, "history.state"));

    previous_entry = controller.GetLastCommittedEntry();
    previous_frame_entry = previous_entry->GetFrameEntry(child);

    // Navigate the subframe (renderer-initiated) to the same URL it's currently
    // on.
    FrameNavigateParamsCapturer capturer(child);
    ASSERT_TRUE(NavigateToURLFromRenderer(child, child_url));
    capturer.Wait();

    // We reused the previous NavigationEntry and FNE, but replaced the entry in
    // the renderer.
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(previous_frame_entry,
              controller.GetLastCommittedEntry()->GetFrameEntry(child));
    EXPECT_TRUE(capturer.did_replace_entry());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(child, "history.state"));
  }

  {
    // Replace history.state to "foo".
    ReplaceState(child, "foo");
    EXPECT_EQ("foo", EvalJs(child, "history.state"));

    previous_entry = controller.GetLastCommittedEntry();
    previous_frame_entry = previous_entry->GetFrameEntry(child);

    // Navigate the subframe (browser-initiated) to the same URL it's currently
    // on.
    FrameNavigateParamsCapturer capturer(child);
    ASSERT_TRUE(NavigateFrameToURL(child, child_url));
    capturer.Wait();

    // The navigation got converted into a reload - we reused the previous
    // NavigationEntry, FNE, and didn't do replacement in the renderer.
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(previous_frame_entry,
              controller.GetLastCommittedEntry()->GetFrameEntry(child));
    EXPECT_FALSE(capturer.did_replace_entry());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(child, "history.state"));
  }

  // 3) Test navigating the subframe to the same URL, but it ends up in an error
  // page due to network error.
  {
    // Replace history.state to "foo".
    ReplaceState(child, "foo");
    EXPECT_EQ("foo", EvalJs(child, "history.state"));

    previous_entry = controller.GetLastCommittedEntry();
    previous_frame_entry = previous_entry->GetFrameEntry(child);

    // Navigate the subframe (browser-initiated) to the same URL it's currently
    // on, but end up in an error page instead.
    auto url_loader_interceptor = std::make_unique<URLLoaderInterceptor>(
        base::BindRepeating([](URLLoaderInterceptor::RequestParams* params) {
          network::URLLoaderCompletionStatus status;
          status.error_code = net::ERR_NOT_IMPLEMENTED;
          params->client->OnComplete(status);
          return true;
        }));

    FrameNavigateParamsCapturer capturer(child);
    ASSERT_FALSE(NavigateFrameToURL(child, child_url));
    capturer.Wait();

    // We reused the previous NavigationEntry and FNE, but replaced the entry in
    // the renderer.
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(previous_frame_entry,
              controller.GetLastCommittedEntry()->GetFrameEntry(child));
    EXPECT_TRUE(capturer.did_replace_entry());
  }

  // 4) Test successfully navigating the subframe to the same URL after a failed
  // navigation.
  {
    previous_entry = controller.GetLastCommittedEntry();
    previous_frame_entry = previous_entry->GetFrameEntry(child);

    // Navigate the subframe (browser-initiated) to the same URL it's currently
    // on successfully (instead of ending up in an error page again).
    TestNavigationObserver observer(shell()->web_contents());
    FrameNavigateParamsCapturer capturer(child);
    ASSERT_TRUE(NavigateFrameToURL(child, child_url));
    capturer.Wait();
    EXPECT_TRUE(observer.last_navigation_succeeded());

    // The navigation got converted into a reload - we reused the previous
    // NavigationEntry, FNE, and didn't do replacement in the renderer.
    // TODO(https://crbug.com/1188956): Once error-page isolation for subframes
    // is turned on, this should do replacement.
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(previous_frame_entry,
              controller.GetLastCommittedEntry()->GetFrameEntry(child));
    EXPECT_FALSE(capturer.did_replace_entry());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(child, "history.state"));
  }
}

// Navigating a subframe to the same URL when the URL has a fragment
// should not append a new history entry.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NoHistoryOnNavigationToSameURLWithFragment) {
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL(
                   "a.com", "/cross_site_iframe_factory.html?a(a)")));

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* child = contents()->GetPrimaryFrameTree().root()->child_at(0);
  GURL child_url = child->current_url();

  {
    // Navigate the subframe to a fragment.
    TestNavigationObserver observer(shell()->web_contents());
    FrameNavigateParamsCapturer capturer(child);
    EXPECT_TRUE(ExecJs(child, "location.href = '#bar';"));
    capturer.Wait();
    observer.Wait();
    EXPECT_TRUE(observer.last_navigation_succeeded());
    EXPECT_TRUE(capturer.is_same_document());
    child_url = child->current_url();
  }

  // Replace history.state to "foo".
  ReplaceState(child, "foo");
  EXPECT_EQ("foo", EvalJs(child, "history.state"));

  NavigationEntryImpl* previous_entry = controller.GetLastCommittedEntry();
  scoped_refptr<FrameNavigationEntry> previous_frame_entry =
      previous_entry->GetFrameEntry(child);

  EXPECT_EQ(2, controller.GetEntryCount());

  {
    // Navigate to the same URL (browser-initiated).
    FrameNavigateParamsCapturer capturer(child);
    EXPECT_TRUE(NavigateFrameToURL(child, child_url));
    capturer.Wait();
    // We're classifying this as AUTO_SUBFRAME because the navigation got
    // converted into a reload.
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());

    // Since we did a reload, it's not classified as a same-document navigation.
    EXPECT_FALSE(capturer.is_same_document());

    // We reuse the last committed entry for this navigation.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(previous_frame_entry,
              controller.GetLastCommittedEntry()->GetFrameEntry(child));
    EXPECT_EQ(2, controller.GetEntryCount());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(child, "history.state"));
  }

  {
    // Replace history.state to "foo".
    ReplaceState(child, "foo");
    EXPECT_EQ("foo", EvalJs(child, "history.state"));

    previous_entry = controller.GetLastCommittedEntry();
    previous_frame_entry = previous_entry->GetFrameEntry(child);

    // Navigate to the same URL (renderer-initiated).
    FrameNavigateParamsCapturer capturer(child);
    EXPECT_TRUE(NavigateToURLFromRenderer(child, child_url));
    capturer.Wait();

    // We did a same-document navigation.
    EXPECT_TRUE(capturer.is_same_document());

    // We reused the previous NavigationEntry and FNE, but replaced the entry in
    // the renderer.
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(previous_frame_entry,
              controller.GetLastCommittedEntry()->GetFrameEntry(child));

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(child, "history.state"));

    EXPECT_EQ(2, controller.GetEntryCount());
  }
}

// Reloading a subframe should not append a new history entry.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NoHistoryOnSubframeReload) {
  // Navigate to a page with a cross-site subframe.
  ASSERT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL(
                   "a.com", "/cross_site_iframe_factory.html?a(b)")));

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  FrameTreeNode* child = root->child_at(0);

  // Replace history.state to "foo".
  ReplaceState(child, "foo");
  EXPECT_EQ("foo", EvalJs(child, "history.state"));

  EXPECT_EQ(1, controller.GetEntryCount());
  NavigationEntryImpl* previous_entry = controller.GetLastCommittedEntry();
  scoped_refptr<FrameNavigationEntry> previous_frame_entry =
      previous_entry->GetFrameEntry(child);

  {
    // Reload the subframe (browser-initiated).
    FrameNavigateParamsCapturer capturer(child);
    child->current_frame_host()->Reload();
    capturer.Wait();
    // We're classifying this as AUTO_SUBFRAME.
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());

    // We reused the previous NavigationEntry, FNE, and didn't do replacement in
    // the renderer.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(previous_frame_entry,
              controller.GetLastCommittedEntry()->GetFrameEntry(child));
    EXPECT_EQ(1, controller.GetEntryCount());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(child, "history.state"));
  }

  {
    // Replace history.state to "foo".
    ReplaceState(child, "foo");
    EXPECT_EQ("foo", EvalJs(child, "history.state"));

    previous_entry = controller.GetLastCommittedEntry();
    previous_frame_entry = previous_entry->GetFrameEntry(child);

    // Reload the subframe (renderer-initiated).
    FrameNavigateParamsCapturer capturer(child);
    EXPECT_TRUE(ExecJs(child, "location.reload()"));
    capturer.Wait();

    // We're classifying this as AUTO_SUBFRAME.
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());

    // We reused the previous NavigationEntry, FNE, and didn't do replacement in
    // the renderer.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(previous_frame_entry,
              controller.GetLastCommittedEntry()->GetFrameEntry(child));
    EXPECT_EQ(1, controller.GetEntryCount());

    // We keep the same history.state value.
    EXPECT_EQ("foo", EvalJs(child, "history.state"));
  }

  {
    // Replace history.state to "foo".
    ReplaceState(child, "foo");
    EXPECT_EQ("foo", EvalJs(child, "history.state"));

    previous_entry = controller.GetLastCommittedEntry();
    previous_frame_entry = previous_entry->GetFrameEntry(child);

    // Reload the subframe (browser-initiated), but this time we hit a network
    // error and end up in an error page.
    auto url_loader_interceptor = std::make_unique<URLLoaderInterceptor>(
        base::BindRepeating([](URLLoaderInterceptor::RequestParams* params) {
          network::URLLoaderCompletionStatus status;
          status.error_code = net::ERR_NOT_IMPLEMENTED;
          params->client->OnComplete(status);
          return true;
        }));
    TestNavigationObserver reload_observer(shell()->web_contents());
    FrameNavigateParamsCapturer capturer(child);
    child->current_frame_host()->Reload();
    capturer.Wait();
    EXPECT_FALSE(reload_observer.last_navigation_succeeded());

    // We're classifying this as AUTO_SUBFRAME.
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());

    // We reused the previous NavigationEntry, FNE, and didn't do replacement in
    // the renderer.
    // TODO(https://crbug.com/1188956): Once error-page isolation for subframes
    // is turned on, this should do replacement.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(previous_frame_entry,
              controller.GetLastCommittedEntry()->GetFrameEntry(child));
    EXPECT_EQ(1, controller.GetEntryCount());

    url_loader_interceptor.reset();
  }

  {
    previous_entry = controller.GetLastCommittedEntry();
    previous_frame_entry = previous_entry->GetFrameEntry(child);

    // Reload the subframe (browser-initiated) after a failed navigation.
    TestNavigationObserver reload_observer(shell()->web_contents());
    FrameNavigateParamsCapturer capturer(child);
    child->current_frame_host()->Reload();
    capturer.Wait();
    EXPECT_TRUE(reload_observer.last_navigation_succeeded());

    // We're classifying this as AUTO_SUBFRAME.
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());

    // We reused the previous NavigationEntry, FNE, and didn't do replacement in
    // the renderer.
    // TODO(https://crbug.com/1188956): Once error-page isolation for subframes
    // is turned on, this should do replacement.
    EXPECT_FALSE(capturer.did_replace_entry());
    EXPECT_EQ(previous_entry, controller.GetLastCommittedEntry());
    EXPECT_EQ(previous_frame_entry,
              controller.GetLastCommittedEntry()->GetFrameEntry(child));
    EXPECT_EQ(1, controller.GetEntryCount());

    // We keep the history.state value from before the failed navigation.
    // TODO(http://crbug.com/1188956): Ensure error page isolation correctly
    // maintains history.state as well.
    if (SiteIsolationPolicy::IsErrorPageIsolationEnabled(false)) {
      EXPECT_EQ(nullptr, EvalJs(child, "history.state"));
    } else {
      EXPECT_EQ("foo", EvalJs(child, "history.state"));
    }
  }
}

// Verify that if a history navigation only affects a subframe that was
// removed, the main frame should not be reloaded.  See
// httos://crbug.com/705550.  This test checks the case where the attempted
// subframe navigation was same-document.
//
// TODO(alexmos, creis): Consider changing this behavior to auto-traverse
// history to the first entry which finds a frame to navigate.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       GoBackSameDocumentInRemovedSubframe) {
  GURL main_url = embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b,c)");
  ASSERT_TRUE(NavigateToURL(shell(), main_url));

  NavigationControllerImpl& controller = contents()->GetController();
  ASSERT_EQ(1, controller.GetEntryCount());

  FrameTreeNode* ftn_a = contents()->GetPrimaryFrameTree().root();
  FrameTreeNode* ftn_b = ftn_a->child_at(0);
  FrameTreeNode* ftn_c = ftn_a->child_at(1);

  // Set some state in the main frame that we can check later to make sure it
  // wasn't reloaded.
  EXPECT_TRUE(ExecJs(ftn_a, "window.state = 'a';"));

  // history.pushState() in the main frame.
  GURL ps2_url(embedded_test_server()->GetURL("a.com", "/ps2.html"));
  {
    FrameNavigateParamsCapturer capturer(ftn_a);
    ASSERT_TRUE(ExecJs(ftn_a, "history.pushState({}, 'page 2', 'ps2.html')"));
    capturer.Wait();
    EXPECT_EQ(2, controller.GetEntryCount());
    EXPECT_EQ(ps2_url, controller.GetLastCommittedEntry()->GetURL());
  }

  // history.pushState() twice in the c subframe.
  {
    FrameNavigateParamsCapturer capturer(ftn_c);
    ASSERT_TRUE(ExecJs(ftn_c, "history.pushState({}, 'page 3', 'ps3.html')"));
    capturer.Wait();
    EXPECT_EQ(3, controller.GetEntryCount());
  }
  {
    FrameNavigateParamsCapturer capturer(ftn_c);
    ASSERT_TRUE(ExecJs(ftn_c, "history.pushState({}, 'page 4', 'ps4.html')"));
    capturer.Wait();
    EXPECT_EQ(4, controller.GetEntryCount());
  }

  // Navigate frame b cross-document.
  GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
  {
    TestNavigationObserver navigation_observer(contents());
    EXPECT_TRUE(NavigateToURLFromRenderer(ftn_b, url_b));
    navigation_observer.WaitForNavigationFinished();
  }
  EXPECT_EQ(5, controller.GetEntryCount());

  // Go back.  This navigates frame b back, and we should be at next-to-last
  // entry (of 5 total) after that's done.
  EXPECT_EQ(4, controller.GetCurrentEntryIndex());
  {
    TestNavigationObserver navigation_observer(shell()->web_contents());
    shell()->GoBackOrForward(-1);
    navigation_observer.Wait();
  }
  EXPECT_EQ(5, controller.GetEntryCount());
  EXPECT_EQ(3, controller.GetCurrentEntryIndex());

  // Last committed entry's URL should be ps2.html, corresponding to latest
  // navigation in the main frame.
  EXPECT_EQ(ps2_url, controller.GetLastCommittedEntry()->GetURL());

  // Set some state in frame b that we can check later to make sure it wasn't
  // reloaded.
  EXPECT_TRUE(ExecJs(ftn_b, "window.state='b';"));

  // Remove the c subframe.
  RenderFrameDeletedObserver deleted_observer(ftn_c->current_frame_host());
  EXPECT_TRUE(ExecJs(ftn_a,
                     "var f = document.querySelectorAll('iframe')[1];"
                     "f.parentNode.removeChild(f);"));
  deleted_observer.WaitUntilDeleted();

  // Try going back.  The target of this navigation had been a same-document
  // navigation in subframe c (to ps3.html), but since c has been removed, this
  // shouldn't reload frames a or b.  Frame |a| also shouldn't fire redundant
  // popstate events.
  EXPECT_TRUE(ExecJs(ftn_a, "window.popstateCalled = false"));
  EXPECT_TRUE(ExecJs(
      ftn_a, "window.onpopstate = () => { window.popstateCalled = true; }"));
  shell()->GoBackOrForward(-1);
  EXPECT_TRUE(WaitForLoadStop(contents()));
  EXPECT_EQ(ps2_url, controller.GetLastCommittedEntry()->GetURL());
  EXPECT_EQ("a", EvalJs(ftn_a, "window.state"));
  EXPECT_EQ("b", EvalJs(ftn_b, "window.state"));
  EXPECT_EQ(false, EvalJs(ftn_a, "window.popstateCalled"));

  // The corresponding NavigationEntry should now be the current one.
  EXPECT_EQ(5, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetCurrentEntryIndex());

  // Try going back again.  Similarly, this would've gone back to c's original
  // URL when it was loaded from main_url, but since c is removed, this
  // shouldn't reload frames a or b or fire popstate events.
  shell()->GoBackOrForward(-1);
  EXPECT_TRUE(WaitForLoadStop(contents()));
  EXPECT_EQ(ps2_url, controller.GetLastCommittedEntry()->GetURL());
  EXPECT_EQ("a", EvalJs(ftn_a, "window.state"));
  EXPECT_EQ("b", EvalJs(ftn_b, "window.state"));
  EXPECT_EQ(false, EvalJs(ftn_a, "window.popstateCalled"));
  EXPECT_EQ(5, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());

  // Going back now should result in a same-document navigation in the main
  // frame to main_url.
  shell()->GoBackOrForward(-1);
  EXPECT_TRUE(WaitForLoadStop(contents()));
  EXPECT_EQ(main_url, controller.GetLastCommittedEntry()->GetURL());
  EXPECT_EQ("a", EvalJs(ftn_a, "window.state"));
  EXPECT_EQ("b", EvalJs(ftn_b, "window.state"));
  EXPECT_EQ(true, EvalJs(ftn_a, "window.popstateCalled"));
  EXPECT_EQ(5, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());

  // Go forward.  This should navigate the main frame same-document to
  // ps2.html.
  {
    FrameNavigateParamsCapturer capturer(ftn_a);
    shell()->GoBackOrForward(1);
    capturer.Wait();
  }
  EXPECT_EQ(5, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
  EXPECT_EQ(ps2_url, ftn_a->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ("a", EvalJs(ftn_a, "window.state"));
  EXPECT_EQ("b", EvalJs(ftn_b, "window.state"));

  // Go forward three steps.  This should navigate the subframe b
  // cross-document to url_b (erasing its window.state).
  {
    FrameNavigateParamsCapturer capturer(ftn_b);
    shell()->GoBackOrForward(3);
    capturer.Wait();
  }
  EXPECT_EQ(url_b, ftn_b->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(5, controller.GetEntryCount());
  EXPECT_EQ(4, controller.GetCurrentEntryIndex());
  EXPECT_EQ("a", EvalJs(ftn_a, "window.state"));
  EXPECT_EQ(nullptr, EvalJs(ftn_b, "window.state"));
}

// Verify that if a history navigation only affects a subframe that was
// removed, the main frame should not be reloaded. See
// httos://crbug.com/705550.  This test is similar to the one above, but checks
// the case where the attempted subframe navigation was cross-document rather
// than same-document.
//
// TODO(alexmos, creis): Consider changing this behavior to auto-traverse
// history to the first entry which finds a frame to navigate.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       GoBackCrossDocumentInRemovedSubframe) {
  GURL main_url = embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)");
  ASSERT_TRUE(NavigateToURL(shell(), main_url));

  NavigationControllerImpl& controller = contents()->GetController();
  ASSERT_EQ(1, controller.GetEntryCount());

  FrameTreeNode* ftn_a = contents()->GetPrimaryFrameTree().root();
  FrameTreeNode* ftn_b = ftn_a->child_at(0);

  // Set some state in the main frame that we can check later to make sure it
  // wasn't reloaded.
  EXPECT_TRUE(ExecJs(ftn_a, "window.state = 'a';"));

  // Navigate frame b cross-document.
  GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
  {
    TestNavigationObserver navigation_observer(contents());
    EXPECT_TRUE(NavigateToURLFromRenderer(ftn_b, url_b));
    navigation_observer.WaitForNavigationFinished();
  }
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());

  // Delete frame b.
  RenderFrameDeletedObserver deleted_observer(
      ftn_a->child_at(0)->current_frame_host());
  EXPECT_TRUE(ExecJs(ftn_a,
                     "var f = document.querySelector('iframe');"
                     "f.parentNode.removeChild(f);"));
  deleted_observer.WaitUntilDeleted();
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());

  // Go back.  Since this history navigation targets a non-existent subframe,
  // the main frame shouldn't be reloaded, and the corresponding
  // NavigationEntry should become the current one.
  shell()->GoBackOrForward(-1);
  EXPECT_TRUE(WaitForLoadStop(contents()));
  EXPECT_EQ(main_url, controller.GetLastCommittedEntry()->GetURL());
  EXPECT_EQ("a", EvalJs(ftn_a, "window.state"));
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());
  EXPECT_FALSE(controller.CanGoBack());
  EXPECT_TRUE(controller.CanGoForward());

  // Go forward and expect similar behavior.
  shell()->GoBackOrForward(1);
  EXPECT_TRUE(WaitForLoadStop(contents()));
  EXPECT_EQ(main_url, controller.GetLastCommittedEntry()->GetURL());
  EXPECT_EQ("a", EvalJs(ftn_a, "window.state"));
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
  EXPECT_TRUE(controller.CanGoBack());
  EXPECT_FALSE(controller.CanGoForward());
}

// This test is similar to the one above, but checks the case where the first
// attempted navigation after subframe removal is a forward navigation
// rather than a back navigation.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       GoForwardCrossDocumentInRemovedSubframe) {
  GURL main_url = embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)");
  ASSERT_TRUE(NavigateToURL(shell(), main_url));

  NavigationControllerImpl& controller = contents()->GetController();
  ASSERT_EQ(1, controller.GetEntryCount());

  FrameTreeNode* ftn_a = contents()->GetPrimaryFrameTree().root();
  FrameTreeNode* ftn_b = contents()->GetPrimaryFrameTree().root()->child_at(0);
  GURL orig_subframe_url(ftn_b->current_frame_host()->GetLastCommittedURL());

  // Set some state in the main frame that we can check later to make sure it
  // wasn't reloaded.
  EXPECT_TRUE(ExecJs(ftn_a, "window.state = 'a';"));

  // Navigate frame b cross-document twice.
  GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
  {
    TestNavigationObserver navigation_observer(contents());
    EXPECT_TRUE(NavigateToURLFromRenderer(ftn_b, url_b));
    navigation_observer.WaitForNavigationFinished();
  }
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
  GURL url_c(embedded_test_server()->GetURL("c.com", "/title2.html"));
  {
    TestNavigationObserver navigation_observer(contents());
    EXPECT_TRUE(NavigateToURLFromRenderer(ftn_b, url_c));
    navigation_observer.WaitForNavigationFinished();
  }
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetCurrentEntryIndex());

  // history.pushState() in the main frame.
  GURL ps_url(embedded_test_server()->GetURL("a.com", "/ps.html"));
  {
    FrameNavigateParamsCapturer capturer(ftn_a);
    ASSERT_TRUE(
        ExecJs(ftn_a, "history.pushState({}, 'push state', 'ps.html')"));
    capturer.Wait();
    EXPECT_EQ(4, controller.GetEntryCount());
    EXPECT_EQ(ps_url, controller.GetLastCommittedEntry()->GetURL());
  }

  // Go back three stops, bringing back the original URL in the subframe.
  // (Note that going back by three steps all at once won't work as expected
  // due to https://crbug.com/542299, where finding that the main frame needs
  // to navigate same-document ignores any subframe navigations that should
  // also be part of the history entry navigation.)
  shell()->GoBackOrForward(-1);
  EXPECT_TRUE(WaitForLoadStop(contents()));
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetCurrentEntryIndex());
  EXPECT_EQ(main_url, controller.GetLastCommittedEntry()->GetURL());
  shell()->GoBackOrForward(-2);
  EXPECT_TRUE(WaitForLoadStop(contents()));
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());
  EXPECT_EQ(orig_subframe_url,
            ftn_b->current_frame_host()->GetLastCommittedURL());

  // Delete frame b.
  RenderFrameDeletedObserver deleted_observer(
      ftn_a->child_at(0)->current_frame_host());
  EXPECT_TRUE(ExecJs(ftn_a,
                     "var f = document.querySelector('iframe');"
                     "f.parentNode.removeChild(f);"));
  deleted_observer.WaitUntilDeleted();
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());

  // Go forward.  Since this navigation attempt targets a non-existent
  // subframe, the main frame shouldn't be reloaded, and the corresponding
  // NavigationEntry should become current.  There should be no redundant
  // popstate events.
  EXPECT_TRUE(ExecJs(ftn_a, "window.popstateCalled = false"));
  EXPECT_TRUE(ExecJs(
      ftn_a, "window.onpopstate = () => { window.popstateCalled = true; }"));
  shell()->GoBackOrForward(1);
  EXPECT_TRUE(WaitForLoadStop(contents()));
  EXPECT_EQ("a", EvalJs(ftn_a, "window.state"));
  EXPECT_EQ(false, EvalJs(ftn_a, "window.popstateCalled"));
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());

  // Go forward again and expect similar behavior.
  shell()->GoBackOrForward(1);
  EXPECT_TRUE(WaitForLoadStop(contents()));
  EXPECT_EQ("a", EvalJs(ftn_a, "window.state"));
  EXPECT_EQ(false, EvalJs(ftn_a, "window.popstateCalled"));
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetCurrentEntryIndex());

  // Go forward yet again.  This should result in a same-document navigation in
  // the main frame to ps.html.
  shell()->GoBackOrForward(1);
  EXPECT_TRUE(WaitForLoadStop(contents()));
  EXPECT_EQ(true, EvalJs(ftn_a, "window.popstateCalled"));
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(3, controller.GetCurrentEntryIndex());
  EXPECT_TRUE(controller.CanGoBack());
  EXPECT_FALSE(controller.CanGoForward());
  EXPECT_EQ(ps_url, controller.GetLastCommittedEntry()->GetURL());
}

// Check that if we ignore a history entry that targets a removed subframe, the
// entry still stays around and is used properly when the subframe gets
// recreated.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RestoreRemovedSubframe) {
  // Start on a page with a same-site iframe.  It's important that this iframe
  // isn't dynamically inserted for history navigations in this test.
  GURL main_url =
      embedded_test_server()->GetURL("a.com", "/page_with_iframe.html");
  ASSERT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = contents()->GetController();
  FrameTreeNode* ftn_a = contents()->GetPrimaryFrameTree().root();
  FrameTreeNode* ftn_b = ftn_a->child_at(0);
  EXPECT_EQ(embedded_test_server()->GetURL("a.com", "/title1.html"),
            ftn_b->current_frame_host()->GetLastCommittedURL());

  // Set some state in the main frame that we can check later to make sure it
  // wasn't reloaded.
  EXPECT_TRUE(ExecJs(ftn_a, "window.state = 'a';"));

  // Navigate subframe cross-site twice.
  GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
  {
    TestNavigationObserver navigation_observer(contents());
    EXPECT_TRUE(NavigateToURLFromRenderer(ftn_b, url_b));
    navigation_observer.WaitForNavigationFinished();
  }

  GURL url_c(embedded_test_server()->GetURL("c.com", "/title3.html"));
  {
    TestNavigationObserver navigation_observer(contents());
    EXPECT_TRUE(NavigateToURLFromRenderer(ftn_b, url_c));
    navigation_observer.WaitForNavigationFinished();
  }
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetCurrentEntryIndex());

  // Remove subframe.
  RenderFrameDeletedObserver deleted_observer(
      ftn_a->child_at(0)->current_frame_host());
  EXPECT_TRUE(ExecJs(ftn_a,
                     "var f = document.querySelector('iframe');"
                     "f.parentNode.removeChild(f);"));
  deleted_observer.WaitUntilDeleted();

  // Go back.  This normally attempts to navigate the subframe from url_c to
  // url_b, but the subframe no longer exists.  Check that the main frame isn't
  // reloaded.
  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(contents()));
  EXPECT_EQ("a", EvalJs(ftn_a, "window.state"));
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());

  // The test assumes the current page and its iframes gets deleted after
  // navigation and later recreated on the back navigation. Disable back/forward
  // cache to ensure that it doesn't get preserved in the cache.
  DisableBackForwardCacheForTesting(shell()->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  // Navigate main frame to another url.
  EXPECT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("a.com", "/title2.html")));
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetCurrentEntryIndex());

  // Now navigate back.  This should go back to |main_url|, reloading the
  // subframe at |url_b|, which is its URL in the history entry that we ignored
  // during the last back navigation above.
  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(contents()));
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
  EXPECT_EQ(url_b,
            ftn_a->child_at(0)->current_frame_host()->GetLastCommittedURL());
}

// Check that when we go back in a subframe on a page that contains another
// frame which is crashed, we not only go back in the subframe but also reload
// the sad frame.  This restores restore the state covered by the corresponding
// NavigationEntry more faithfully.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ReloadSadFrameWithSubframeHistoryNavigation) {
  // Ensure this test runs in full site-per-process mode so that we can get a
  // sad frame on Android.
  IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());

  // Start on a page with two iframes.
  GURL main_url = embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b,c)");
  ASSERT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = contents()->GetController();
  FrameTreeNode* ftn_a = contents()->GetPrimaryFrameTree().root();
  FrameTreeNode* ftn_b = ftn_a->child_at(0);
  FrameTreeNode* ftn_c = ftn_a->child_at(1);
  GURL url_b(ftn_b->current_frame_host()->GetLastCommittedURL());
  GURL url_c(ftn_c->current_frame_host()->GetLastCommittedURL());

  // Navigate first subframe cross-site.
  GURL url_d(embedded_test_server()->GetURL("d.com", "/title2.html"));
  {
    TestNavigationObserver navigation_observer(contents());
    EXPECT_TRUE(NavigateToURLFromRenderer(ftn_b, url_d));
    navigation_observer.WaitForNavigationFinished();
  }
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
  EXPECT_EQ(url_d, ftn_b->current_frame_host()->GetLastCommittedURL());

  // Crash second subframe.
  RenderProcessHost* process_c = ftn_c->current_frame_host()->GetProcess();
  RenderProcessHostWatcher crash_observer(
      process_c, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  process_c->Shutdown(0);
  crash_observer.Wait();
  EXPECT_FALSE(ftn_c->current_frame_host()->IsRenderFrameLive());
  EXPECT_TRUE(ftn_c->current_frame_host()->GetLastCommittedURL().is_empty());

  // Go back.  This should navigate the first subframe back to b.com, and it
  // should also restore the subframe in c.com.
  controller.GoBack();
  EXPECT_TRUE(WaitForLoadStop(contents()));
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());
  EXPECT_EQ(url_b, ftn_b->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(url_c, ftn_c->current_frame_host()->GetLastCommittedURL());
  EXPECT_TRUE(ftn_c->current_frame_host()->IsRenderFrameLive());
}

// Regression test for https://crbug.com/1088354, where a different-document
// load was incorrectly scheduled for a history navigation in a subframe that
// had no existing and no target FrameNavigationEntry.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SubframeGoesBackAndSiblingHasNoFrameEntry) {
  // Start on a page with a same-site iframe.
  GURL main_url =
      embedded_test_server()->GetURL("a.com", "/page_with_iframe.html");
  ASSERT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = contents()->GetController();
  FrameTreeNode* ftn_a = contents()->GetPrimaryFrameTree().root();
  FrameTreeNode* ftn_b = ftn_a->child_at(0);
  EXPECT_EQ(embedded_test_server()->GetURL("a.com", "/title1.html"),
            ftn_b->current_frame_host()->GetLastCommittedURL());

  // Add a second subframe dynamically and set some state on it to ensure it's
  // not reloaded.  Using a javascript: URL results in the renderer not sending
  // a DidCommitNavigation IPC back for the new frame, leaving it without a
  // FrameNavigationEntry.
  EXPECT_TRUE(ExecJs(ftn_a,
                     "var f = document.createElement('iframe');"
                     "f.src = 'javascript:void(0)';"
                     "document.body.appendChild(f);"));
  FrameTreeNode* ftn_c = ftn_a->child_at(1);
  EXPECT_TRUE(ExecJs(ftn_c, "window.state='c';"));

  // Navigate first subframe same-document.
  {
    FrameNavigateParamsCapturer capturer(ftn_b);
    EXPECT_TRUE(ExecJs(ftn_b, "location.hash = 'foo'"));
    capturer.Wait();
    EXPECT_TRUE(capturer.is_same_document());
  }

  // Go back in the first subframe.  This should navigate the first subframe
  // back same-document, while the second subframe shouldn't be reloaded, and
  // the history navigation shouldn't crash while processing it.
  {
    FrameNavigateParamsCapturer capturer(ftn_b);
    controller.GoBack();
    capturer.Wait();
    EXPECT_TRUE(capturer.is_same_document());
    EXPECT_TRUE(WaitForLoadStop(contents()));
    EXPECT_EQ("c", EvalJs(ftn_c, "window.state"));
  }
}

// Checks that a browser-initiated same-document navigation on a page which has
// a valid base URL preserves the base URL.
// See https://crbug.com/1082141.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       LoadDataWithBaseURLSameDocumentNavigation) {
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;

  const GURL base_url("http://baseurl");
  const GURL history_url("http://history");
  const std::string data = "<html><title>One</title><body>foo</body></html>";
  const GURL data_url = GURL("data:text/html;charset=utf-8," + data);

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  {
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    shell()->LoadDataWithBaseURL(history_url, data, base_url);
    same_tab_observer.Wait();
  }

  // Verify the last committed NavigationEntry.
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(history_url, entry->GetVirtualURL());
  EXPECT_EQ(history_url, entry->GetHistoryURLForDataURL());
  EXPECT_EQ(data_url, entry->GetURL());

  {
    // Make a same-document navigation via history.pushState.
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    EXPECT_TRUE(ExecJs(shell(), "history.pushState('', 'test', '#')"));
    same_tab_observer.Wait();
  }

  // Verify the last committed NavigationEntry.
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(history_url, entry->GetVirtualURL());
  EXPECT_EQ(history_url, entry->GetHistoryURLForDataURL());
  EXPECT_EQ(data_url, entry->GetURL());

  {
    // Go back.
    TestNavigationObserver back_load_observer(shell()->web_contents());
    controller.GoBack();
    back_load_observer.Wait();
  }

  // Verify the last committed NavigationEntry.
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(history_url, entry->GetVirtualURL());
  EXPECT_EQ(history_url, entry->GetHistoryURLForDataURL());
  EXPECT_EQ(data_url, entry->GetURL());
  EXPECT_EQ(base_url, EvalJs(shell(), "document.URL"));
}

// Checks that a browser-initiated same-document navigation on a page which has
// a valid base URL preserves the base URL.
// See https://crbug.com/1082141.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RendererInitiatedBackToLoadDataWithBaseURL) {
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting())
    return;

  const GURL base_url("http://baseurl");
  const GURL history_url("http://history");
  const std::string data = "<html><title>One</title><body>foo</body></html>";
  const GURL data_url = GURL("data:text/html;charset=utf-8," + data);

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  {
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    shell()->LoadDataWithBaseURL(history_url, data, base_url);
    same_tab_observer.Wait();
  }

  // Verify the last committed NavigationEntry.
  NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(history_url, entry->GetVirtualURL());
  EXPECT_EQ(history_url, entry->GetHistoryURLForDataURL());
  EXPECT_EQ(data_url, entry->GetURL());

  {
    // Make a same-document navigation via history.pushState.
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    EXPECT_TRUE(ExecJs(shell(), "history.pushState('', 'test', '#')"));
    same_tab_observer.Wait();
  }

  // Verify the last committed NavigationEntry.
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(history_url, entry->GetVirtualURL());
  EXPECT_EQ(history_url, entry->GetHistoryURLForDataURL());
  EXPECT_EQ(data_url, entry->GetURL());

  {
    // Renderer-initiated-back.
    FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                              ->GetPrimaryFrameTree()
                              .root();
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.back()"));
    capturer.Wait();
  }

  // Verify the last committed NavigationEntry.
  entry = controller.GetLastCommittedEntry();
  EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
  EXPECT_EQ(history_url, entry->GetVirtualURL());
  EXPECT_EQ(history_url, entry->GetHistoryURLForDataURL());
  EXPECT_EQ(data_url, entry->GetURL());
  EXPECT_EQ(base_url, EvalJs(shell(), "document.URL"));
}

// Checks that a renderer-initiated back/forward navigation to a page which has
// a valid base URL does not crash during restore.
// See https://crbug.com/1082141.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    RendererInitiatedBackToLoadDataWithBaseURLDuringRestore) {
  // LoadDataWithBaseURL is never subject to --site-per-process policy today
  // (this API is only used by Android WebView [where OOPIFs have not shipped
  // yet] and GuestView cases [which always hosts guests inside a renderer
  // without an origin lock]).  Therefore, skip the test in --site-per-process
  // mode to avoid renderer kills which won't happen in practice as described
  // above.
  //
  // TODO(https://crbug.com/962643): Consider enabling this test once Android
  // Webview or WebView guests support OOPIFs and/or origin locks.
  if (AreAllSitesIsolatedForTesting()) {
    return;
  }

  const GURL base_url("http://baseurl");
  const GURL history_url("http://history");
  const std::string data = "<html><title>One</title><body>foo</body></html>";
  const GURL data_url = GURL("data:text/html;charset=utf-8," + data);

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  {
    TestNavigationObserver same_tab_observer(shell()->web_contents(), 1);
    shell()->LoadDataWithBaseURL(history_url, data, base_url);
    same_tab_observer.Wait();
  }

  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  ASSERT_TRUE(NavigateToURL(shell(), url2));

  // Restore into a new shell.
  Shell* restore_shell = Shell::CreateNewWindow(
      controller.GetBrowserContext(), GURL::EmptyGURL(), nullptr, gfx::Size());
  NavigationControllerImpl& restore_controller =
      static_cast<NavigationControllerImpl&>(
          restore_shell->web_contents()->GetController());
  restore_controller.CopyStateFrom(&controller, true /* needs_reload */);
  EXPECT_EQ(2, restore_controller.GetEntryCount());
  EXPECT_EQ(1, restore_controller.GetLastCommittedEntryIndex());
  {
    TestNavigationObserver restore_observer(restore_shell->web_contents());
    restore_controller.LoadIfNecessary();
    restore_observer.Wait();
  }

  {
    // Renderer-initiated-back to data url with base url. This should not crash.
    FrameTreeNode* root =
        static_cast<WebContentsImpl*>(restore_shell->web_contents())
            ->GetPrimaryFrameTree()
            .root();
    FrameNavigateParamsCapturer capturer(root);
    EXPECT_TRUE(ExecJs(root, "history.back()"));
    capturer.Wait();
  }
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       HistoryNavigationInNewSubframe) {
  // This test specifically observes behavior of creating a new frame during a
  // history navigation, so disable the back forward cache.
  DisableBackForwardCacheForTesting(contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Navigate to a page with an iframe.
  GURL url1 =
      embedded_test_server()->GetURL("/frame_tree/page_with_one_frame.html");
  ASSERT_TRUE(NavigateToURL(shell(), url1));
  GURL iframe_url = root->child_at(0)->current_url();

  // Navigate away so the iframe is destroyed.
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  ASSERT_TRUE(NavigateToURL(shell(), url2));

  // Go back (browser-initiated).
  ASSERT_TRUE(controller.CanGoBack());
  TestNavigationManager observer(shell()->web_contents(), iframe_url);
  controller.GoBack();
  EXPECT_TRUE(observer.WaitForRequestStart());

  // Check the initial navigation for the new iframe.
  NavigationRequest* navigation = root->child_at(0)->navigation_request();
  ASSERT_TRUE(navigation);
  EXPECT_TRUE(navigation->IsRendererInitiated());
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       HistoryNavigationInNewSubframeRendererInitiated) {
  // This test specifically observes behavior of creating a new frame during a
  // history navigation, so disable the back forward cache.
  DisableBackForwardCacheForTesting(contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Navigate to a page with an iframe.
  GURL url1 =
      embedded_test_server()->GetURL("/frame_tree/page_with_one_frame.html");
  ASSERT_TRUE(NavigateToURL(shell(), url1));
  GURL iframe_url = root->child_at(0)->current_url();

  // Navigate away so the iframe is destroyed.
  GURL url2(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  ASSERT_TRUE(NavigateToURL(shell(), url2));

  // Go back (renderer-initiated).
  ASSERT_TRUE(controller.CanGoBack());
  TestNavigationManager observer(shell()->web_contents(), iframe_url);

  FrameNavigateParamsCapturer capturer(root);
  EXPECT_TRUE(ExecJs(root, "history.back()"));
  EXPECT_TRUE(observer.WaitForRequestStart());

  // Check the initial navigation for the new iframe.
  NavigationRequest* navigation = root->child_at(0)->navigation_request();
  ASSERT_TRUE(navigation);
  EXPECT_TRUE(navigation->IsRendererInitiated());
}

// Navigate an iframe, then reload it. Check the navigation and the
// FrameNavigationEntry are the same in both cases.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, ReloadFrame) {
  GURL main_url = embedded_test_server()->GetURL(
      "a.com", "/frame_tree/page_with_one_frame.html");
  GURL iframe_url = embedded_test_server()->GetURL("b.com", "/title1.html");
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());

  ASSERT_TRUE(NavigateToURL(shell(), main_url));
  RenderFrameHostImpl* main_frame =
      static_cast<WebContentsImpl*>(shell()->web_contents())
          ->GetPrimaryMainFrame();

  // 1. Navigate the iframe using POST, initiated from the main frame.
  TestNavigationManager observer_1(shell()->web_contents(), iframe_url);
  EXPECT_TRUE(ExecJs(main_frame, JsReplace(R"(
    var form = document.createElement('form');
    form.method = 'POST';
    form.action = $1;
    form.target = "child-name-0";
    document.body.appendChild(form);
    form.submit();
  )",
                                           iframe_url)));

  EXPECT_TRUE(observer_1.WaitForRequestStart());

  // Check the navigation (initial navigation).
  NavigationRequest* navigation_1 =
      main_frame->child_at(0)->navigation_request();
  ASSERT_TRUE(navigation_1);
  EXPECT_EQ(main_url.DeprecatedGetOriginAsURL(),
            navigation_1->GetReferrer().url);
  EXPECT_EQ(network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin,
            navigation_1->GetReferrer().policy);
  EXPECT_TRUE(navigation_1->IsRendererInitiated());
  EXPECT_TRUE(navigation_1->IsPost());

  // Check the FrameNavigationEntry (initial navigation).
  ASSERT_TRUE(observer_1.WaitForNavigationFinished());
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  NavigationEntryImpl* entry_1 = controller.GetLastCommittedEntry();
  ASSERT_EQ(1U, entry_1->root_node()->children.size());
  scoped_refptr<FrameNavigationEntry> frame_entry_1 =
      entry_1->root_node()->children[0]->frame_entry.get();
  absl::optional<url::Origin> origin_1 = frame_entry_1->initiator_origin();
  ASSERT_TRUE(frame_entry_1->initiator_origin().has_value());
  EXPECT_EQ(url::Origin::Create(main_url),
            frame_entry_1->initiator_origin().value());
  content::Referrer referrer_1 = frame_entry_1->referrer();
  EXPECT_EQ(main_url.DeprecatedGetOriginAsURL(), frame_entry_1->referrer().url);
  EXPECT_EQ(network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin,
            frame_entry_1->referrer().policy);
  int item_sequence_number_1 = frame_entry_1->item_sequence_number();
  int document_sequence_number_1 = frame_entry_1->document_sequence_number();

  // 2. Reload the document.
  TestNavigationManager observer_2(shell()->web_contents(), iframe_url);
  main_frame->child_at(0)->current_frame_host()->Reload();

  // Check the navigation (reload).
  EXPECT_TRUE(observer_2.WaitForRequestStart());
  NavigationRequest* navigation_2 =
      main_frame->child_at(0)->navigation_request();
  ASSERT_TRUE(navigation_2);
  EXPECT_EQ(main_url.DeprecatedGetOriginAsURL(),
            navigation_2->GetReferrer().url);
  EXPECT_EQ(network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin,
            navigation_2->GetReferrer().policy);
  EXPECT_FALSE(navigation_2->IsRendererInitiated());
  EXPECT_TRUE(navigation_2->IsPost());

  // Check the FrameNavigationEntry (reload).
  ASSERT_TRUE(observer_2.WaitForNavigationFinished());
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  NavigationEntryImpl* entry_2 = controller.GetLastCommittedEntry();
  ASSERT_EQ(1U, entry_1->root_node()->children.size());
  scoped_refptr<FrameNavigationEntry> frame_entry_2 =
      entry_2->root_node()->children[0]->frame_entry.get();
  absl::optional<url::Origin> origin_2 = frame_entry_2->initiator_origin();
  ASSERT_TRUE(frame_entry_2->initiator_origin().has_value());
  EXPECT_EQ(url::Origin::Create(main_url),
            frame_entry_2->initiator_origin().value());
  content::Referrer referrer_2 = frame_entry_1->referrer();
  EXPECT_EQ(main_url.DeprecatedGetOriginAsURL(), frame_entry_2->referrer().url);
  EXPECT_EQ(network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin,
            frame_entry_2->referrer().policy);

  int item_sequence_number_2 = frame_entry_1->item_sequence_number();
  int document_sequence_number_2 = frame_entry_1->document_sequence_number();
  EXPECT_EQ(item_sequence_number_1, item_sequence_number_2);
  EXPECT_EQ(document_sequence_number_1, document_sequence_number_2);
}

// A history navigation only navigates the iframe that should be changed to
// update history. A grandchild iframe on the initial about:blank document will
// not commit any navigation and should not be modified by a history navigation
// in another frame.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       HistoryNavigationDoesntMoveFrameWithoutCommit) {
  WebContents* wc = shell()->web_contents();
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(wc->GetController());

  GURL main_url = embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(a,a)");
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  RenderFrameHostImpl* main_frame =
      static_cast<RenderFrameHostImpl*>(wc->GetPrimaryMainFrame());
  FrameTreeNode* b_frame = main_frame->child_at(0);
  FrameTreeNode* c_frame = main_frame->child_at(1);
  ASSERT_TRUE(b_frame);
  ASSERT_TRUE(c_frame);

  {
    LoadCommittedCapturer capturer(wc);
    EXPECT_TRUE(ExecJs(c_frame, kAddEmptyFrameScript));
    capturer.Wait();
  }

  FrameTreeNode* cc_frame = c_frame->current_frame_host()->child_at(0);
  ASSERT_TRUE(cc_frame);

  const char set_status_on_beforeunload[] = R"(
      window.top.testStatus = "STARTED";
      window.addEventListener(
        "beforeunload",
        () => { window.top.testStatus = "UNLOAD" },
        false);
      window.top.testStatus;
      )";
  EXPECT_EQ("STARTED",
            EvalJs(cc_frame, set_status_on_beforeunload).ExtractString());

  // Navigate frame 'b' creating a new history entry.
  GURL url2 = embedded_test_server()->GetURL("a.com", "/title2.html");
  EXPECT_TRUE(NavigateToURLFromRenderer(b_frame, url2));

  // Frame 'cc' is a grandchild frame left at the initial about:blank document.
  // This results in it not being committed.
  EXPECT_FALSE(cc_frame->current_frame_host()->has_committed_any_navigation());

  scoped_refptr<FrameNavigationEntry> b_entry =
      controller.GetLastCommittedEntry()->GetFrameEntry(b_frame);
  int64_t b_isn = b_entry->item_sequence_number();

  // Go back.
  FrameNavigateParamsCapturer capturer(b_frame);
  controller.GoBack();
  capturer.Wait();

  scoped_refptr<FrameNavigationEntry> b2_entry =
      controller.GetLastCommittedEntry()->GetFrameEntry(b_frame);
  int64_t b2_isn = b2_entry->item_sequence_number();

  // Frame 'b' should have been navigated back.
  EXPECT_NE(b_isn, b2_isn);
  // Frame 'c' should not have committed due to 'b' navigating.
  EXPECT_FALSE(cc_frame->current_frame_host()->has_committed_any_navigation());

  // We bounce through frame 'cc' in order to avoid races with beforeunload.
  //
  // If frame 'b' navigating caused the un-committed about:blank frame to do a
  // navigation, then it would have to finish its commit and fire beforeunload,
  // which would result in getting "UNLOAD" here. This comes from the original
  // repro at https://crbug.com/1192709.
  const char set_status_done[] = R"(
      if (window.top.testStatus == "STARTED")
        window.top.testStatus = "DONE";
      window.top.testStatus;
      )";
  EXPECT_EQ("DONE", EvalJs(cc_frame, set_status_done).ExtractString());
}

// If the NavigationEntry |is_overriding_user_agent_| attribute is updated and
// the entry reloaded, the new document will load with the user agent
// overridden. Consecutive navigation will continue with the user agent
// overridden.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       OverrideUserAgentThenReload) {
  EXPECT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("a.com", "/empty.html")));
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());

  // Update the entry and load it.
  controller.GetLastCommittedEntry()->SetIsOverridingUserAgent(true);
  EXPECT_TRUE(controller.GetLastCommittedEntry()->GetIsOverridingUserAgent());
  controller.Reload(ReloadType::ORIGINAL_REQUEST_URL, true);
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_TRUE(controller.GetLastCommittedEntry()->GetIsOverridingUserAgent());

  // Consecutive same-document navigation uses the new state.
  EXPECT_TRUE(ExecJs(shell(), "history.pushState('', 'test', '#foo2')"));
  EXPECT_TRUE(controller.GetLastCommittedEntry()->GetIsOverridingUserAgent());

  // Consecutive cross-document same-origin navigation uses the new state.
  EXPECT_TRUE(ExecJs(shell(), "location.href = '/title1.html';"));
  EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
  EXPECT_TRUE(controller.GetLastCommittedEntry()->GetIsOverridingUserAgent());

  // Consecutive cross-document cross-origin navigation uses the new state.
  EXPECT_TRUE(ExecJs(
      shell(), JsReplace("location.href = $1;", embedded_test_server()->GetURL(
                                                    "b.com", "/empty.html"))));
  EXPECT_TRUE(controller.GetLastCommittedEntry()->GetIsOverridingUserAgent());
}

// If the NavigationEntry |is_overriding_user_agent_| attribute is updated and
// the document is still the one loaded without overriding user agent, then
// committing a same-document navigation will restore the value used by the
// document.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       OverrideUserAgentThenSameDocument) {
  EXPECT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("a.com", "/empty.html")));
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());

  controller.GetLastCommittedEntry()->SetIsOverridingUserAgent(true);
  EXPECT_TRUE(controller.GetLastCommittedEntry()->GetIsOverridingUserAgent());
  EXPECT_TRUE(ExecJs(shell(), "history.pushState('', 'test', '#foo1')"));
  EXPECT_FALSE(controller.GetLastCommittedEntry()->GetIsOverridingUserAgent());
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       URLLoadReturnsNavigationHandle) {
  GURL url(embedded_test_server()->GetURL("/title1.html"));

  TestNavigationManager navigation_manager(shell()->web_contents(), url);
  base::WeakPtr<NavigationHandle> navigation =
      shell()->web_contents()->GetController().LoadURLWithParams(
          NavigationController::LoadURLParams(url));

  // The returned NavigationHandle should be valid.
  EXPECT_TRUE(navigation);

  // Start the navigation and ensure that the NavigationHandle we saw matches
  // the one TestNavigationManager saw.
  ASSERT_TRUE(navigation_manager.WaitForRequestStart());
  EXPECT_TRUE(navigation);
  EXPECT_EQ(navigation->GetNavigationId(),
            navigation_manager.GetNavigationHandle()->GetNavigationId());

  // Commit navigation and ensure that the weak ptr to NavigationHandle was
  // invalidated.
  ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
  EXPECT_FALSE(navigation);
}

// All non-about:blank/about:srcdoc about: URLs reported by the renderer should
// get rewritten to about:blank#blocked (browser-initiated navigations will be
// blocked from starting entirely, and trying to trigger the navigation in test
// will hit a NOTREACHED() in the NavigationRequest constructor).
// See ChildProcessSecurityPolicyImpl::CanRequestURL() for a discussion.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, FilterAbout) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  // Navigate to the initial page.
  EXPECT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("a.com", "/empty.html")));
  EXPECT_EQ(1, controller.GetEntryCount());

  // Renderer-initiated navigation will be rewritten to "about:blank#blocked".
  EXPECT_TRUE(NavigateToURLFromRenderer(shell(), GURL("about:cache"),
                                        GURL(kBlockedURL)));
  EXPECT_EQ(2, controller.GetEntryCount());
  ASSERT_TRUE(controller.GetVisibleEntry());
  EXPECT_EQ(GURL(kBlockedURL), controller.GetVisibleEntry()->GetURL());
}

// Tests that reload() does not create a new NavigationEntry.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, WindowReload) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  // Navigate to the initial page.
  EXPECT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("a.com", "/title1.html")));
  EXPECT_EQ(1, controller.GetEntryCount());

  TestNavigationObserver observer(contents());
  EXPECT_TRUE(ExecJs(current_main_frame_host(), "window.location.reload();"));
  observer.WaitForNavigationFinished();
  // Reloads in the main frame have a valid nav_entry_id.
  EXPECT_NE(0, observer.last_nav_entry_id());
  EXPECT_EQ(1, controller.GetEntryCount());
}

// Tests that reload() in iframes does not create a new NavigationEntry.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, WindowReloadInIframe) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  // Navigate to the initial page.
  EXPECT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
                                         "a.com", "/page_with_iframe.html")));
  EXPECT_EQ(1, controller.GetEntryCount());

  TestNavigationObserver observer(contents());
  EXPECT_TRUE(ExecJs(current_main_frame_host()->child_at(0),
                     "window.location.reload();"));
  observer.WaitForNavigationFinished();
  // Reloads in subframes do not have a valid nav_entry_id.
  EXPECT_EQ(0, observer.last_nav_entry_id());
  EXPECT_EQ(1, controller.GetEntryCount());
}

namespace {
// Tracks how many NavigationStateChanged call were triggered with the
// INVALIDATE_TYPE_ALL flag.
class AllNavigationStateChangedDelegate : public WebContentsDelegate {
 public:
  AllNavigationStateChangedDelegate() = default;

  AllNavigationStateChangedDelegate(const AllNavigationStateChangedDelegate&) =
      delete;
  AllNavigationStateChangedDelegate& operator=(
      const AllNavigationStateChangedDelegate&) = delete;

  ~AllNavigationStateChangedDelegate() override = default;

  void NavigationStateChanged(WebContents* source,
                              InvalidateTypes changed_flags) override {
    if (changed_flags == INVALIDATE_TYPE_ALL)
      call_count_++;
  }

  int call_count() { return call_count_; }

 private:
  int call_count_ = 0;
};
}  // namespace

// Test to highlight the difference in behavior for relative urls in an
// about:blank popup. The expectations in this test can be directly compared to
// those for the "Navigate the popup main frame to a same-document URL." case
// in the test NavigationStateChangedForInitialNavigationEntry immediately
// below.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RelativeURLInAboutBlankPopup) {
  GURL url1 = embedded_test_server()->GetURL("/title1.html");
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  // Pop open a new window without specifying a URL.
  ShellAddedObserver new_shell_observer;
  EXPECT_TRUE(ExecJs(root, "var w = window.open()"));
  Shell* new_shell = new_shell_observer.GetShell();
  WebContentsImpl* new_contents =
      static_cast<WebContentsImpl*>(new_shell->web_contents());
  EXPECT_TRUE(WaitForLoadStop(new_contents));

  // Observe NavigationStateChanged calls.
  AllNavigationStateChangedDelegate all_navigation_state_changed_delegate;
  new_contents->SetDelegate(&all_navigation_state_changed_delegate);
  EXPECT_EQ(0, all_navigation_state_changed_delegate.call_count());
  ASSERT_NE(new_contents, shell()->web_contents());
  FrameTreeNode* new_root = new_contents->GetPrimaryFrameTree().root();

  NavigationControllerImpl& controller = new_contents->GetController();
  // The new window is on the initial NavigationEntry.
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_TRUE(controller.GetLastCommittedEntry()->IsInitialEntry());

  if (blink::features::IsNewBaseUrlInheritanceBehaviorEnabled()) {
    EXPECT_NE("about:blank",
              EvalJs(new_root, "document.baseURI").ExtractString());
  } else {
    EXPECT_EQ("about:blank",
              EvalJs(new_root, "document.baseURI").ExtractString());
  }

  // Navigate the popup main frame to a same-document relative URL.
  FrameNavigateParamsCapturer capturer(new_root);
  EXPECT_TRUE(ExecJs(new_root, "location.href = '#foo';"));
  capturer.Wait();

  if (blink::features::IsNewBaseUrlInheritanceBehaviorEnabled()) {
    // When an about:blank popup inherits its base url from the initiator,
    // it will not be able to do same-document navigations without specifying an
    // absolute url.

    EXPECT_EQ(embedded_test_server()->GetURL("/title1.html#foo"),
              new_root->current_url());
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_FALSE(capturer.is_same_document());

    // The navigation is a cross-document navigation from the initial empty
    // document, so we committed a new non-initial NavigationEntry that replaced
    // the initial entry.
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_FALSE(controller.GetLastCommittedEntry()->IsInitialEntry());
  } else {
    // When the new inheritance behavior is disabled, an about:blank popup will
    // have about:blank for the baseURI also, and so relative URLs can be used
    // for same document navigations.

    EXPECT_EQ(GURL("about:blank#foo"), new_root->current_url());
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());

    // The navigation is a same-document navigation from the initial empty
    // document, so we committed a new initial NavigationEntry that replaced the
    // previous initial NavigationEntry.
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_TRUE(controller.GetLastCommittedEntry()->IsInitialEntry());
  }
}

// Tests that committing iframes and doing same-document navigations with the
// initial NavigationEntry dispatches the expected NavigationStateChanged calls.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NavigationStateChangedForInitialNavigationEntry) {
  GURL url1 = embedded_test_server()->GetURL("/title1.html");
  GURL url2 = embedded_test_server()->GetURL("/title2.html");
  EXPECT_TRUE(NavigateToURL(shell(), url1));
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  // Pop open a new window without specifying a URL.
  ShellAddedObserver new_shell_observer;
  EXPECT_TRUE(ExecJs(root, "var w = window.open()"));
  Shell* new_shell = new_shell_observer.GetShell();
  WebContentsImpl* new_contents =
      static_cast<WebContentsImpl*>(new_shell->web_contents());
  EXPECT_TRUE(WaitForLoadStop(new_contents));

  // Observe NavigationStateChanged calls.
  AllNavigationStateChangedDelegate all_navigation_state_changed_delegate;
  new_contents->SetDelegate(&all_navigation_state_changed_delegate);
  EXPECT_EQ(0, all_navigation_state_changed_delegate.call_count());
  ASSERT_NE(new_contents, shell()->web_contents());
  FrameTreeNode* new_root = new_contents->GetPrimaryFrameTree().root();

  NavigationControllerImpl& controller = new_contents->GetController();
  // The new window is on the initial NavigationEntry.
  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_TRUE(controller.GetLastCommittedEntry()->IsInitialEntry());

  {
    // Make a new iframe in the popup.
    LoadCommittedCapturer capturer(new_contents);
    std::string script = JsReplace(kAddFrameWithSrcScript, url1);
    EXPECT_TRUE(ExecJs(new_root, script));
    capturer.Wait();
    EXPECT_EQ(url1, new_root->child_at(0)->current_url());

    // The new window is still on the initial NavigationEntry, as the previous
    // navigation modified the existing entry.
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_TRUE(controller.GetLastCommittedEntry()->IsInitialEntry());

    // No new NavigationEntry was committed as the last navigation only modifies
    // the existing NavigationEntry, so no NavigationStateChanged calls were
    // triggered.
    EXPECT_EQ(0, all_navigation_state_changed_delegate.call_count());
  }

  {
    //  Navigate the iframe to another URL.
    FrameNavigateParamsCapturer capturer(new_root->child_at(0));
    NavigateFrameToURL(new_root->child_at(0), url2);
    capturer.Wait();
    EXPECT_EQ(url2, new_root->child_at(0)->current_url());
    EXPECT_EQ(NAVIGATION_TYPE_AUTO_SUBFRAME, capturer.navigation_type());
    // The new window is still on the initial NavigationEntry, as the previous
    // navigation modified the existing entry.
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_TRUE(controller.GetLastCommittedEntry()->IsInitialEntry());

    // No new NavigationEntry was committed as the last navigation only modifies
    // the existing NavigationEntry, so no NavigationStateChanged calls were
    // triggered.
    EXPECT_EQ(0, all_navigation_state_changed_delegate.call_count());
  }

  {
    // Navigate the popup main frame to a same-document URL.
    FrameNavigateParamsCapturer capturer(new_root);
    EXPECT_TRUE(ExecJs(new_root, "location.href = 'about:blank#foo';"));
    capturer.Wait();
    EXPECT_EQ(GURL("about:blank#foo"), new_root->current_url());
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_EXISTING_ENTRY,
              capturer.navigation_type());
    EXPECT_TRUE(capturer.is_same_document());

    // The navigation is a same-document navigation from the initial empty
    // document, so we committed a new initial NavigationEntry that replaced the
    // previous initial NavigationEntry.
    EXPECT_TRUE(capturer.did_replace_entry());
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_TRUE(controller.GetLastCommittedEntry()->IsInitialEntry());

    // 2 INVALIDATE_TYPE_ALL NavigationStateChanged calls were triggered when
    // committing the new NavigationEntry which keeps the "initial" status:
    // #1 was triggered by DiscardNonCommittedEntries().
    // #2 is triggered by NotifyNavigationEntryCommitted().
    // Note that this is different from the _Ignore test below, which wouldn't
    // fire the events because the client chooses to ignore the updates.
    EXPECT_EQ(2, all_navigation_state_changed_delegate.call_count());
  }

  {
    // Navigate the popup main frame to another URL.
    FrameNavigateParamsCapturer capturer(new_root);
    EXPECT_TRUE(ExecJs(new_root, JsReplace("location.href = $1;", url2)));
    capturer.Wait();
    EXPECT_EQ(url2, new_root->current_url());
    EXPECT_EQ(NAVIGATION_TYPE_MAIN_FRAME_NEW_ENTRY, capturer.navigation_type());
    EXPECT_TRUE(capturer.did_replace_entry());

    // The navigation is a cross-document navigation from the initial empty
    // document, so we committed a new non-initial NavigationEntry that
    // replaced the initial entry.
    EXPECT_EQ(1, controller.GetEntryCount());
    EXPECT_FALSE(controller.GetLastCommittedEntry()->IsInitialEntry());

    // 2 additional INVALIDATE_TYPE_ALL NavigationStateChanged calls were
    // triggered (increasing the count to 4), and they're not for the initial
    // NavigationEntry.
    // #1 was triggered by DiscardNonCommittedEntries().
    // #2 is triggered by NotifyNavigationEntryCommitted().
    EXPECT_EQ(4, all_navigation_state_changed_delegate.call_count());
  }
}

// Verify that calling Restore() with a non-empty entry list on a new
// NavigationController will not result in an initial NavigationEntry.
// See https://crbug.com/1295723.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    RestoreRemovesInitialEmptyDocumentOnFreshNavigationController) {
  GURL url_1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_2(embedded_test_server()->GetURL("a.com", "/title2.html"));

  // 1. Start in a tab with 1 NavigationEntry.
  EXPECT_TRUE(NavigateToURL(shell(), url_1));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_EQ(url_1, root->current_url());

  EXPECT_EQ(1, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
  NavigationEntryImpl* entry1 = controller.GetLastCommittedEntry();

  // 2. Create a NavigationEntry with the same PageState as |entry1|.
  std::unique_ptr<NavigationEntryImpl> restored_entry =
      NavigationEntryImpl::FromNavigationEntry(
          NavigationController::CreateNavigationEntry(
              url_1, Referrer(), absl::nullopt /* initiator_origin= */,
              /* initiator_base_url= */ absl::nullopt,
              ui::PAGE_TRANSITION_RELOAD, false, std::string(),
              controller.GetBrowserContext(),
              nullptr /* blob_url_loader_factory */));
  EXPECT_EQ(0U, restored_entry->root_node()->children.size());
  std::unique_ptr<NavigationEntryRestoreContextImpl> context =
      std::make_unique<NavigationEntryRestoreContextImpl>();
  restored_entry->SetPageState(entry1->GetPageState(), context.get());

  // 3. Create a new tab and don't navigate it anywhere.
  std::vector<std::unique_ptr<NavigationEntry>> entries;
  entries.push_back(std::move(restored_entry));
  Shell* new_shell = Shell::CreateNewWindow(
      controller.GetBrowserContext(), GURL::EmptyGURL(), nullptr, gfx::Size());
  FrameTreeNode* new_root =
      static_cast<WebContentsImpl*>(new_shell->web_contents())
          ->GetPrimaryFrameTree()
          .root();
  NavigationControllerImpl& new_controller =
      static_cast<NavigationControllerImpl&>(
          new_shell->web_contents()->GetController());
  // The new tab's root is on the initial NavigationEntry and is marked as
  // "on initial empty document" as it hasn't navigated anywhere.
  EXPECT_TRUE(new_root->is_on_initial_empty_document());
  EXPECT_TRUE(new_controller.GetLastCommittedEntry()->IsInitialEntry());

  // 4. Call Restore() with the entry from the old tab.
  new_controller.Restore(entries.size() - 1, RestoreType::kRestored, &entries);
  EXPECT_EQ(0u, entries.size());
  EXPECT_EQ(1, new_controller.GetEntryCount());
  // The new tab's root is no longer on the initial NavigationEntry but is still
  // marked as "on initial empty document" after calling Restore().
  EXPECT_TRUE(new_root->is_on_initial_empty_document());
  EXPECT_FALSE(new_controller.GetLastCommittedEntry()->IsInitialEntry());

  // 5. Navigate the new tab to another URL.
  FrameNavigateParamsCapturer capturer(new_root);
  EXPECT_TRUE(NavigateToURL(new_shell, url_2));
  capturer.Wait();
  // The navigation should append a new entry instead of replacing the restored
  // entry (which is what it would've done if it was still on the
  // initial NavigationEntry).
  EXPECT_FALSE(capturer.did_replace_entry());
  EXPECT_EQ(2, new_controller.GetEntryCount());
  EXPECT_EQ(url_1, new_controller.GetEntryAtIndex(0)->GetURL());
  EXPECT_EQ(url_2, new_controller.GetLastCommittedEntry()->GetURL());
}

// Verify that calling Restore with an empty set of entries on a fresh/unused
// NavigationController does not crash, since this may be possible via Android
// WebView's restoreState() API. See https://crbug.com/1287624.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       RestoreEmptyAndLoadOnFreshNavigationController) {
  // 1. Create a new tab with a new NavigationController, and don't navigate it
  // anywhere.
  Shell* new_shell =
      Shell::CreateNewWindow(shell()->web_contents()->GetBrowserContext(),
                             GURL::EmptyGURL(), nullptr, gfx::Size());
  FrameTreeNode* new_root =
      static_cast<WebContentsImpl*>(new_shell->web_contents())
          ->GetPrimaryFrameTree()
          .root();
  NavigationControllerImpl& new_controller =
      static_cast<NavigationControllerImpl&>(
          new_shell->web_contents()->GetController());
  // The new tab is on the initial NavigationEntry.
  EXPECT_EQ(1, new_controller.GetEntryCount());
  EXPECT_EQ(0, new_controller.GetLastCommittedEntryIndex());
  EXPECT_TRUE(new_controller.GetLastCommittedEntry()->IsInitialEntry());

  // 2. Restore an empty set of entries in the new tab, which is possible via
  // Android WebView's restoreState API.
  std::vector<std::unique_ptr<NavigationEntry>> entries;
  new_controller.Restore(-1, RestoreType::kRestored, &entries);
  // There is always a valid index, and the initial NavigationEntry is kept to
  // ensure that there is always at least 1 entry.
  EXPECT_EQ(1, new_controller.GetEntryCount());
  EXPECT_EQ(0, new_controller.GetLastCommittedEntryIndex());
  EXPECT_TRUE(new_controller.GetLastCommittedEntry()->IsInitialEntry());

  // 3. Call LoadIfNecessary() in the new tab.
  ASSERT_EQ(0u, entries.size());
  new_controller.LoadIfNecessary();
  // No navigation had started, as there's no non-initial NavigationEntry to
  // load.
  EXPECT_FALSE(new_root->navigation_request());
  EXPECT_EQ(nullptr, new_controller.GetPendingEntry());

  // 4. Navigate the new tab to another URL.
  FrameNavigateParamsCapturer capturer(new_root);
  GURL url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(new_shell, url));
  capturer.Wait();
  // The navigation should replace the initial NavigationEntry .
  EXPECT_TRUE(capturer.did_replace_entry());
  EXPECT_EQ(1, new_controller.GetEntryCount());
  EXPECT_EQ(url, new_controller.GetLastCommittedEntry()->GetURL());
}

// A test to verify that a navigation with |force_new_browsing_instance| set to
// true results in a new BrowsingInstance, even if it's to a URL compatible with
// the original SiteInstance.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ResetForTestForcesNewBrowsingInstance) {
  GURL test_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), test_url));

  SiteInstance* initial_site_instance = contents()->GetSiteInstance();

  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  DisableProactiveBrowsingInstanceSwapFor(root->current_frame_host());

  TestNavigationObserver navigation_observer(contents());
  NavigationController::LoadURLParams params(test_url);
  params.force_new_browsing_instance = true;
  contents()->GetController().LoadURLWithParams(params);
  navigation_observer.Wait();
  EXPECT_TRUE(navigation_observer.last_navigation_succeeded());

  SiteInstance* post_reset_site_instance = contents()->GetSiteInstance();
  EXPECT_NE(initial_site_instance, post_reset_site_instance);
  EXPECT_FALSE(
      post_reset_site_instance->IsRelatedSiteInstance(initial_site_instance));
}

// Test that unload timing of old document is accessible after same-origin
// navigation, but not after cross-origin navigation.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, UnloadTiming) {
  // With BackForwardCache, old document doesn't fire unload handlers as the
  // page is stored in BackForwardCache on navigation.
  DisableBackForwardCacheForTesting(contents(),
                                    BackForwardCache::TEST_USES_UNLOAD_EVENT);

  GURL initial_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(a)"));
  GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
  // Navigate to a page with an iframe.
  EXPECT_TRUE(NavigateToURL(shell(), initial_url));

  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  ASSERT_EQ(1u, root->child_count());
  FrameTreeNode* child = root->child_at(0u);

  // Navigate the subframe same-origin.
  ASSERT_TRUE(NavigateFrameToURL(child, url_a));

  // The unloadEventStart and unloadEventEnd timings should be set.
  EXPECT_TRUE(ExecJs(
      child,
      "var navigationTiming = performance.getEntriesByType('navigation')"));
  ASSERT_EQ(1, EvalJs(child, "navigationTiming.length"));
  EXPECT_NE(0, EvalJs(child, "navigationTiming[0].unloadEventStart"));
  EXPECT_NE(0, EvalJs(child, "navigationTiming[0].unloadEventEnd"));

  // Navigate the subframe cross-origin.
  {
    FrameNavigateParamsCapturer capturer(child);
    ASSERT_TRUE(NavigateFrameToURL(child, url_b));
    capturer.Wait();
  }
  // The unloadEventStart and unloadEventEnd timings should not be set.
  EXPECT_TRUE(ExecJs(
      child,
      "var navigationTiming = performance.getEntriesByType('navigation')"));
  ASSERT_EQ(1, EvalJs(child, "navigationTiming.length"));
  EXPECT_EQ(0, EvalJs(child, "navigationTiming[0].unloadEventStart"));
  EXPECT_EQ(0, EvalJs(child, "navigationTiming[0].unloadEventEnd"));

  // Navigate the main frame same-origin.
  ASSERT_TRUE(NavigateToURL(shell(), url_a));
  // The unloadEventStart and unloadEventEnd timings should be set.
  EXPECT_TRUE(ExecJs(
      root,
      "var navigationTiming = performance.getEntriesByType('navigation')"));
  ASSERT_EQ(1, EvalJs(root, "navigationTiming.length"));
  EXPECT_NE(0, EvalJs(root, "navigationTiming[0].unloadEventStart"));
  EXPECT_NE(0, EvalJs(root, "navigationTiming[0].unloadEventEnd"));

  // Navigate the main frame cross-origin.
  ASSERT_TRUE(NavigateToURL(shell(), url_b));
  // The unloadEventStart and unloadEventEnd timings should not be set.
  EXPECT_TRUE(ExecJs(
      root,
      "var navigationTiming = performance.getEntriesByType('navigation')"));
  ASSERT_EQ(1, EvalJs(root, "navigationTiming.length"));
  EXPECT_EQ(0, EvalJs(root, "navigationTiming[0].unloadEventStart"));
  EXPECT_EQ(0, EvalJs(root, "navigationTiming[0].unloadEventEnd"));
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       SameURLNavigationRetainsHistoryState) {
  GURL main_frame_url(embedded_test_server()->GetURL("/page_with_iframe.html"));
  GURL subframe_url(embedded_test_server()->GetURL("/title1.html"));
  GURL other_url(embedded_test_server()->GetURL("/title2.html"));
  // Navigate to a page with an iframe.
  EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  ASSERT_EQ(1u, root->child_count());
  FrameTreeNode* child = root->child_at(0u);
  EXPECT_EQ(main_frame_url, root->current_url());
  EXPECT_EQ(subframe_url, child->current_url());

  // Do a replaceState on the subframe to set the history.state to "foo".
  ReplaceState(child, "foo");

  // Navigate the subframe to the same URL it is currently on.
  {
    FrameNavigateParamsCapturer capturer(child);
    ASSERT_TRUE(NavigateFrameToURL(child, subframe_url));
    capturer.Wait();
    EXPECT_EQ(subframe_url, child->current_url());
  }
  // Check that the history.state is retained after the navigation.
  EXPECT_EQ("foo", EvalJs(child, "history.state"));

  // Navigate the subframe to a different URL.
  {
    FrameNavigateParamsCapturer capturer(child);
    ASSERT_TRUE(NavigateFrameToURL(child, other_url));
    capturer.Wait();
    EXPECT_EQ(other_url, child->current_url());
  }
  // Check that the history.state is not retained after the navigation.
  EXPECT_EQ(nullptr, EvalJs(child, "history.state"));

  // Do a replaceState on the main frame to set the history.state to "bar".
  ReplaceState(root, "bar");
  EXPECT_EQ("bar", EvalJs(root, "history.state"));

  // Navigate the main frame to the same URL it's currently on.
  ASSERT_TRUE(NavigateToURL(shell(), GURL(root->current_url())));
  // Check that the history.state is retained after the navigation.
  EXPECT_EQ("bar", EvalJs(root, "history.state"));

  // Navigate the main frame to a different URL.
  ASSERT_TRUE(NavigateToURL(shell(), other_url));
  // Check that the history.state is not retained after the navigation.
  EXPECT_EQ(nullptr, EvalJs(root, "history.state"));
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       GoForwardAfterInitialHistoryInChildFrame) {
  // Start on a page with one iframe.
  GURL main_url(embedded_test_server()->GetURL(
      "/navigation_controller/page_with_iframe.html"));
  ASSERT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = contents()->GetController();
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  FrameTreeNode* child = root->child_at(0);

  // Navigate to a real page in the subframe, so that the next navigation will
  // be MANUAL_SUBFRAME.
  GURL subframe_url(embedded_test_server()->GetURL(
      "/navigation_controller/simple_page_1.html"));
  {
    TestNavigationObserver navigation_observer(contents());
    EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), subframe_url));
    navigation_observer.Wait();
  }

  // Navigate subframe same-document.
  {
    TestNavigationObserver navigation_observer(contents());
    std::string script = "location.href = '#foo';";
    EXPECT_TRUE(ExecJs(child, script));
    navigation_observer.Wait();
  }
  GURL child_url(child->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());

  // Navigate root cross-document.
  GURL cross_doc_url(embedded_test_server()->GetURL("a.com", "/title2.html"));
  {
    TestNavigationObserver navigation_observer(contents());
    EXPECT_TRUE(NavigateToURLFromRenderer(root, cross_doc_url));
    navigation_observer.Wait();
  }
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetCurrentEntryIndex());

  // Go back 2, this will restore the subframe.
  {
    TestNavigationObserver navigation_observer(contents());
    controller.GoToOffset(-2);
    navigation_observer.Wait();
  }
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());

  // The NavigationApi in the renderer validates same-document back/forward
  // navigations, and cancels them if it is not aware of the existence of the
  // entry being navigated to. Restoring an iframe during a history traversal of
  // its parent used to cause
  // NavigationControllerImpl::GetNavigationApiHistoryEntryVectors() to
  // miscalculate the committing entry index (thinking it was 1 instead of 0).
  // This meant that index 0 was treated as a back entry (when it should have
  // been current) and the index 1 entry was omitted (when it should have been a
  // forward entry). The NavigationApi therefore had incorrect data to refer to
  // when validating back/forward navigations and was entirely unaware of the
  // entry at index 1, so it would incorrectly block a forward navigation to
  // index 1.
  {
    // Go forward.
    TestNavigationObserver navigation_observer(shell()->web_contents());
    controller.GoForward();
    navigation_observer.Wait();
  }
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
  EXPECT_EQ(child_url,
            root->child_at(0)->current_frame_host()->GetLastCommittedURL());
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       GoBackInMultipleIframesOneFastOneSlow) {
  // Start on a page with two iframes.
  GURL main_url = embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b,c)");
  ASSERT_TRUE(NavigateToURL(shell(), main_url));
  NavigationControllerImpl& controller = contents()->GetController();
  FrameTreeNode* ftn_a = contents()->GetPrimaryFrameTree().root();
  FrameTreeNode* ftn_b = ftn_a->child_at(0);
  FrameTreeNode* ftn_c = ftn_a->child_at(1);
  GURL url_b(ftn_b->current_frame_host()->GetLastCommittedURL());
  GURL url_c(ftn_c->current_frame_host()->GetLastCommittedURL());

  // Navigate second subframe same-document.
  {
    TestNavigationObserver navigation_observer(shell()->web_contents());
    std::string script = "location.href = '#foo';";
    EXPECT_TRUE(ExecJs(ftn_c, script));
    navigation_observer.Wait();
  }
  EXPECT_EQ(2, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());

  // Navigate first subframe cross-document.
  GURL url_b2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  {
    TestNavigationObserver navigation_observer(contents());
    EXPECT_TRUE(NavigateToURLFromRenderer(ftn_b, url_b2));
    navigation_observer.Wait();
  }
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(2, controller.GetCurrentEntryIndex());
  EXPECT_EQ(url_b2, ftn_b->current_frame_host()->GetLastCommittedURL());

  // Navigate second subframe cross-document.
  GURL url_c2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  {
    TestNavigationObserver navigation_observer(contents());
    EXPECT_TRUE(NavigateToURLFromRenderer(ftn_c, url_c2));
    navigation_observer.Wait();
  }
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(3, controller.GetCurrentEntryIndex());
  EXPECT_EQ(url_c2, ftn_c->current_frame_host()->GetLastCommittedURL());

  // Go back 3 so that both subframes navigate, but ensure that ftn_c doesn't
  // commit until after ftn_b is entirely finished.
  FrameTestNavigationManager b_delayer(ftn_b->frame_tree_node_id(),
                                       shell()->web_contents(), url_b);
  FrameTestNavigationManager c_delayer(ftn_c->frame_tree_node_id(),
                                       shell()->web_contents(), url_c);
  controller.GoToOffset(-3);
  ASSERT_TRUE(b_delayer.WaitForNavigationFinished());
  ASSERT_TRUE(c_delayer.WaitForNavigationFinished());

  EXPECT_TRUE(WaitForLoadStop(contents()));
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(0, controller.GetCurrentEntryIndex());
  EXPECT_EQ(url_b, ftn_b->current_frame_host()->GetLastCommittedURL());
  EXPECT_EQ(url_c, ftn_c->current_frame_host()->GetLastCommittedURL());

  // The NavigationApi in the renderer validates same-document back/forward
  // navigations, and cancels them if it is not aware of the existence of the
  // entry being navigated to. The back traversal above with multiple iframes
  // committing at different times used to cause
  // NavigationControllerImpl::GetNavigationApiHistoryEntryVectors() to
  // miscalculate the committing entry index (thinking it was 1 instead of 0).
  // This meant that index 0 was treated as a back entry (when it should have
  // been current) and the index 1 entry was omitted (when it should have been a
  // forward entry). The NavigationApi therefore had incorrect data to refer to
  // when validating back/forward navigations and was entirely unaware of the
  // entry at index 1, so it would incorrectly block a forward navigation to
  // index 1.
  {
    // Go forward.
    TestNavigationObserver navigation_observer(shell()->web_contents());
    controller.GoForward();
    navigation_observer.Wait();
  }
  EXPECT_EQ(4, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       DeleteNavigationEntriesFiresNavigationApiDisposeEvent) {
  GURL url1 = embedded_test_server()->GetURL("a.com", "/title1.html");
  GURL url2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  GURL url3(embedded_test_server()->GetURL("a.com", "/title3.html"));
  ASSERT_TRUE(NavigateToURL(shell(), url1));
  ASSERT_TRUE(NavigateToURL(shell(), url2));
  ASSERT_TRUE(NavigateToURL(shell(), url3));
  NavigationControllerImpl& controller = contents()->GetController();

  // Go back.
  TestNavigationObserver back_load_observer(shell()->web_contents());
  controller.GoBack();
  back_load_observer.Wait();
  EXPECT_EQ(3, controller.GetEntryCount());
  EXPECT_EQ(1, controller.GetCurrentEntryIndex());

  // Insert dispose events on all navigation API entries.
  EXPECT_TRUE(ExecJs(contents(), R"(
      window.dispose0_fired = false;
      window.dispose1_fired = false;
      window.dispose2_fired = false;
      navigation.entries()[0].ondispose = () => window.dispose0_fired = true;
      navigation.entries()[1].ondispose = () => window.dispose2_fired = true;
      navigation.entries()[2].ondispose = () => window.dispose2_fired = true;
    )"));

  // Delete the NavigationEntry for |url1| and ensure the appropriate dispose
  // event fires.
  controller.DeleteNavigationEntries(
      base::BindLambdaForTesting([&](content::NavigationEntry* entry) {
        return entry->GetURL() == url1;
      }));
  EXPECT_EQ(true, EvalJs(shell(), "window.dispose0_fired").ExtractBool());
  EXPECT_EQ(false, EvalJs(shell(), "window.dispose1_fired").ExtractBool());
  EXPECT_EQ(false, EvalJs(shell(), "window.dispose2_fired").ExtractBool());

  // Do the same for |url3|.
  controller.DeleteNavigationEntries(
      base::BindLambdaForTesting([&](content::NavigationEntry* entry) {
        return entry->GetURL() == url3;
      }));
  EXPECT_EQ(true, EvalJs(shell(), "window.dispose0_fired").ExtractBool());
  EXPECT_EQ(false, EvalJs(shell(), "window.dispose1_fired").ExtractBool());
  EXPECT_EQ(true, EvalJs(shell(), "window.dispose2_fired").ExtractBool());
}

IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NavigationApiTraverseCancelledByRaceCondition) {
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  EXPECT_TRUE(NavigateToURL(contents(), url1));
  EXPECT_TRUE(NavigateToURL(contents(), url2));

  // Request navigation.back() in the renderer.
  ExecuteScriptAsync(contents()->GetPrimaryFrameTree().root(),
                     "navigation.back().committed.then("
                     "() => document.title = 'FAIL',"
                     "e => document.title = e.name === 'InvalidStateError'"
                     "   ? 'PASS' : 'WRONG_ERROR_TYPE');");

  // Before the navigation.back() is processed and sent to the browser, remove
  // the NavigationEntry that navigation.back() will request to be navigated to.
  contents()->GetController().PruneAllButLastCommitted();

  // The browser process should notify the renderer that an appropriate entry
  // could not be found, and the renderer should fire a navigateerror event.
  TitleWatcher title_watcher(shell()->web_contents(), u"PASS");
  EXPECT_EQ(u"PASS", title_watcher.WaitAndGetTitle());
}

// Verify that navigation API can be used to traverse to same-origin urls, even
// if they are cross site instance. Regression test for
// https://crbug.com/1431412.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       NavigationApiBackSameOriginDifferentSiteInstance) {
  GURL url1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url1));

  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  scoped_refptr<SiteInstance> initial_site_instance =
      contents()->GetSiteInstance();

  TestNavigationObserver navigation_observer(contents());
  NavigationController::LoadURLParams params(url2);
  params.force_new_browsing_instance = true;
  controller.LoadURLWithParams(params);
  navigation_observer.Wait();
  EXPECT_TRUE(navigation_observer.last_navigation_succeeded());
  EXPECT_NE(initial_site_instance, contents()->GetSiteInstance());

  EXPECT_TRUE(EvalJs(shell(), "navigation.canGoBack").ExtractBool());
  TestNavigationObserver navigation_observer2(contents());
  ExecuteScriptAsync(contents()->GetPrimaryFrameTree().root(),
                     "navigation.back()");
  navigation_observer2.Wait();

  EXPECT_EQ(url1, controller.GetLastCommittedEntry()->GetURL());
  EXPECT_EQ(initial_site_instance, contents()->GetSiteInstance());
}

// Tests that renderer-initiated navigation cancellation from the same JS task
// that created the navigation can still be triggered after WillProcessResponse,
// as the browser defers the navigation until the JS task that started it
// finishes.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTestNoServer,
                       RendererInitiatedCancellationDueToJS) {
  net::test_server::ControllableHttpResponse fetch_response(
      embedded_test_server(), "/fetch");

  ASSERT_TRUE(embedded_test_server()->Start());
  // Navigate to a page with an iframe.
  GURL url1(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(a)"));
  EXPECT_TRUE(NavigateToURL(contents(), url1));

  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  FrameTreeNode* child = root->child_at(0);

  GURL child_url = child->current_url();
  GURL child_url_2(embedded_test_server()->GetURL("a.com", "/title1.html"));

  // Now navigate the child frame to another same-site document, but cancel it
  // from the JS task after a synchronous XHR request that we can control.
  TestNavigationManager nav_manager(contents(), child_url_2);
  ExecuteScriptAsync(child, R"(
      location.href = '/title1.html';
      var request = new XMLHttpRequest();
      request.open("GET", "/fetch", /*async=*/false);
      request.send("");
      var fetch_success = (request.readyState == 4 && request.status == 200);
      window.stop();
    )");

  // The navigation should be able to start, as the synchronous XHR request
  // hasn't completed, and the navigation hasn't been canceled yet.
  EXPECT_TRUE(nav_manager.WaitForRequestStart());
  nav_manager.ResumeNavigation();
  NavigationRequest* request =
      static_cast<NavigationRequest*>(nav_manager.GetNavigationHandle());
  EXPECT_EQ(request, child->navigation_request());

  // The navigation should be able to reach the WillProcessResponse stage,
  // and gets deferred by RendererCancellationThrottle after that. Wait for the
  // first NavigationThrottle deferral.
  base::RunLoop run_loop;
  NavigationThrottleRunner* throttle_runner =
      request->GetNavigationThrottleRunnerForTesting();
  throttle_runner->set_first_deferral_callback_for_testing(
      run_loop.QuitClosure());
  run_loop.Run();

  // Check that the deferral is caused by RendererCancellationThrottle.
  EXPECT_TRUE(request->IsDeferredForTesting());
  EXPECT_STREQ("RendererCancellationThrottle",
               throttle_runner->GetDeferringThrottle()->GetNameForLogging());
  EXPECT_EQ(request->state(), NavigationRequest::WILL_PROCESS_RESPONSE);

  // Unblock the JS task in the renderer by sending the response for the sync
  // XHR request. After that, the renderer will trigger navigation cancellation
  // due to the window.stop() call.
  fetch_response.WaitForRequest();
  fetch_response.Send(net::HTTP_OK, "foo");
  fetch_response.Done();

  // The navigation got canceled without committing.
  ASSERT_TRUE(nav_manager.WaitForNavigationFinished());
  EXPECT_FALSE(nav_manager.was_successful());
  EXPECT_EQ(child_url, child->current_url());
  EXPECT_EQ(true, EvalJs(child, "fetch_success"));
}

// Tests that the crash of the renderer that created a navigation will cancel
// the navigation.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTestNoServer,
                       RendererInitiatedCancellationDueToCrash) {
  net::test_server::ControllableHttpResponse fetch_response(
      embedded_test_server(), "/fetch");

  ASSERT_TRUE(embedded_test_server()->Start());
  // Navigate to a page with an iframe.
  GURL url1(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  EXPECT_TRUE(NavigateToURL(contents(), url1));

  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  FrameTreeNode* child = root->child_at(0);

  GURL child_url = child->current_url();
  GURL child_url_2(embedded_test_server()->GetURL("b.com", "/title1.html"));

  // Now navigate the child frame to another same-site page, but cancel it
  // from the JS task after a synchronous XHR request that we can control.
  TestNavigationManager nav_manager(contents(), child_url_2);
  ExecuteScriptAsync(child, R"(
      location.href = '/title1.html';
      var request = new XMLHttpRequest();
      request.open("GET", "/fetch", /*async=*/false);
      request.send("");
      var fetch_success = (request.readyState == 4 && request.status == 200);
      window.stop();
    )");

  // The navigation should be able to start, as the synchronous XHR request
  // hasn't completed, and the navigation hasn't been canceled yet.
  EXPECT_TRUE(nav_manager.WaitForRequestStart());
  nav_manager.ResumeNavigation();
  NavigationRequest* request =
      static_cast<NavigationRequest*>(nav_manager.GetNavigationHandle());
  EXPECT_EQ(request, child->navigation_request());

  // The navigation should be able to reach the WillProcessResponse stage,
  // and gets deferred by RendererCancellationThrottle after that. Wait for the
  // first NavigationThrottle deferral.
  base::RunLoop run_loop;
  NavigationThrottleRunner* throttle_runner =
      request->GetNavigationThrottleRunnerForTesting();
  throttle_runner->set_first_deferral_callback_for_testing(
      run_loop.QuitClosure());
  run_loop.Run();

  // Check that the deferral is caused by RendererCancellationThrottle.
  EXPECT_TRUE(request->IsDeferredForTesting());
  EXPECT_STREQ("RendererCancellationThrottle",
               throttle_runner->GetDeferringThrottle()->GetNameForLogging());
  EXPECT_EQ(request->state(), NavigationRequest::WILL_PROCESS_RESPONSE);

  // Kill the renderer process that started the navigation.
  RenderProcessHost* child_process = child->current_frame_host()->GetProcess();
  RenderProcessHostWatcher crash_observer(
      child_process, RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  child_process->Shutdown(0);
  crash_observer.Wait();

  // The navigation got canceled without committing.
  ASSERT_TRUE(nav_manager.WaitForNavigationFinished());
  EXPECT_FALSE(nav_manager.was_successful());
  // If the process is not shared with the main frame, the current URL of the
  // child frame is empty after the process crashed. If the process is shared,
  // the child frame will be gone because the main frame's process crashed.
  if (AreAllSitesIsolatedForTesting())
    EXPECT_EQ(GURL(), child->current_url());
}

// Tests that if waiting for a renderer-initiated navigation cancellation takes
// too long, a warning for an unresponsive renderer will be sent.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTestNoServer,
    RendererInitiatedCancellationTimeoutWarnsUnresponsiveRenderer) {
  net::test_server::ControllableHttpResponse fetch_response(
      embedded_test_server(), "/fetch");

  ASSERT_TRUE(embedded_test_server()->Start());
  // Navigate to a page with an iframe.
  GURL url1(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  EXPECT_TRUE(NavigateToURL(contents(), url1));

  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  FrameTreeNode* child = root->child_at(0);

  GURL child_url = child->current_url();
  GURL child_url_2(embedded_test_server()->GetURL("b.com", "/title1.html"));

  // Set the cancellation timeout to a low enough value to wait in the test.
  RendererCancellationThrottle::SetCancellationTimeoutForTesting(
      base::Milliseconds(100));
  UnresponsiveRendererObserver unresponsive_renderer_observer(contents());

  // Now navigate the child frame to another same-site page, but cancel it
  // from the JS task after a synchronous XHR request that we can control.
  TestNavigationManager nav_manager(contents(), child_url_2);
  ExecuteScriptAsync(child, R"(
      location.href = '/title1.html';
      var request = new XMLHttpRequest();
      request.open("GET", "/fetch", /*async=*/false);
      request.send("");
      var fetch_success = (request.readyState == 4 && request.status == 200);
      window.stop();
    )");

  // The navigation should be able to start, as the synchronous XHR request
  // hasn't completed, and the navigation hasn't been canceled yet.
  EXPECT_TRUE(nav_manager.WaitForRequestStart());
  nav_manager.ResumeNavigation();
  NavigationRequest* request =
      static_cast<NavigationRequest*>(nav_manager.GetNavigationHandle());
  EXPECT_EQ(request, child->navigation_request());

  // The navigation should be able to reach the WillProcessResponse stage,
  // and gets deferred by RendererCancellationThrottle after that. Wait for the
  // first NavigationThrottle deferral.
  base::RunLoop run_loop;
  NavigationThrottleRunner* throttle_runner =
      request->GetNavigationThrottleRunnerForTesting();
  throttle_runner->set_first_deferral_callback_for_testing(
      run_loop.QuitClosure());
  run_loop.Run();

  // Check that the deferral is caused by RendererCancellationThrottle.
  EXPECT_TRUE(request->IsDeferredForTesting());
  EXPECT_STREQ("RendererCancellationThrottle",
               throttle_runner->GetDeferringThrottle()->GetNameForLogging());
  EXPECT_EQ(request->state(), NavigationRequest::WILL_PROCESS_RESPONSE);

  // Verify that we will be notified about the unresponsive renderer.
  RenderProcessHost* hung_process = unresponsive_renderer_observer.Wait();
  EXPECT_EQ(hung_process, child->current_frame_host()->GetProcess());

  // Verify that the throttle still waits for the renderer-initiated
  // cancellation.
  EXPECT_TRUE(request->IsDeferredForTesting());

  // Reset the cancellation timeout to the default value.
  RendererCancellationThrottle::SetCancellationTimeoutForTesting(
      base::TimeDelta());
}

// Tests that navigating to a document that was previously sandboxed but later
// on is loaded again without being marked as sandboxed would not reuse the
// opaque origin that was used when the document was first loaded, and instead
// recalculate the origin using the latest state.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTestNoServer,
                       HistoryNavigationToPreviouslySandboxedDocument) {
  net::test_server::ControllableHttpResponse response1(
      embedded_test_server(), "/sandboxed_on_first_load_only");
  net::test_server::ControllableHttpResponse response2(
      embedded_test_server(), "/sandboxed_on_first_load_only");
  ASSERT_TRUE(embedded_test_server()->Start());

  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  FrameTreeNode* root =
      static_cast<WebContentsImpl*>(contents())->GetPrimaryFrameTree().root();

  GURL main_url(
      embedded_test_server()->GetURL("a.com", "/sandboxed_on_first_load_only"));
  {
    // Navigate to a page that is sandboxed when it's first loaded.
    TestNavigationObserver nav_observer(contents());
    shell()->LoadURL(main_url);
    response1.WaitForRequest();
    // Note: we use Cache-Control: no-store to prevent the HTTP cache, so that
    // we can change the response on the next navigation to this page.
    response1.Send(
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/html; charset=utf-8\r\n"
        "Content-Security-Policy: sandbox\r\n"
        "Cache-Control: no-store\r\n"
        "\r\n"
        "sandboxed document");
    response1.Done();
    nav_observer.Wait();
    EXPECT_EQ(main_url, controller.GetLastCommittedEntry()->GetURL());
    // The document is sandboxed, and has an opaque origin derived from
    // `main_url`.
    EXPECT_TRUE(root->current_frame_host()->IsSandboxed(
        network::mojom::WebSandboxFlags::kAll));
    EXPECT_TRUE(root->current_frame_host()->GetLastCommittedOrigin().opaque());
    EXPECT_TRUE(
        root->current_frame_host()->GetLastCommittedOrigin().CanBeDerivedFrom(
            main_url));
  }

  {
    // Navigate to another document.
    TestNavigationObserver back_load_observer(contents());
    GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
    shell()->LoadURL(url_2);
    back_load_observer.Wait();
    EXPECT_EQ(url_2, controller.GetLastCommittedEntry()->GetURL());
  }

  {
    // Navigate back to a page that was sandboxed before, but is no longer
    // sandboxed now (as the HTTP response had changed).
    TestNavigationObserver nav_observer(contents());
    controller.GoBack();
    response2.WaitForRequest();
    response2.Send(
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/html; charset=utf-8\r\n"
        "\r\n"
        "non-sandboxed document");
    response2.Done();
    nav_observer.Wait();
    EXPECT_EQ(main_url, controller.GetLastCommittedEntry()->GetURL());
    // The document is not sandboxed, and has a non-opaque origin based on
    // `main_url`.
    EXPECT_FALSE(root->current_frame_host()->IsSandboxed(
        network::mojom::WebSandboxFlags::kAll));
    EXPECT_FALSE(root->current_frame_host()->GetLastCommittedOrigin().opaque());
    EXPECT_EQ(root->current_frame_host()->GetLastCommittedOrigin(),
              url::Origin::Create(main_url));
  }
}

// Same with above, but with an iframe and the sandbox attribute instead.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       HistoryNavigationToPreviouslySandboxedDocumentOnIframe) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  GURL main_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  FrameTreeNode* root =
      static_cast<WebContentsImpl*>(contents())->GetPrimaryFrameTree().root();

  {
    // Create an iframe and navigate it somewhere, to avoid initial empty
    // document replacement on subsequent navigations.
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, R"(
        let iframe = document.createElement('iframe');
        iframe.id = 'child';
        iframe.src = location.href;
        document.body.appendChild(iframe);)"));
    capturer.Wait();
  }
  FrameTreeNode* child = root->child_at(0);

  {
    // Set the sandbox attribute on the iframe and navigate it to about:blank.
    FrameNavigateParamsCapturer capturer(child);
    EXPECT_TRUE(ExecJs(root, R"(
        document.getElementById('child').setAttribute('sandbox', '');
        document.getElementById('child').src = 'about:blank';)"));
    capturer.Wait();

    // The document is sandboxed, and has an opaque origin derived from
    // `main_url` (the initiator origin).
    EXPECT_TRUE(child->current_frame_host()->IsSandboxed(
        network::mojom::WebSandboxFlags::kAll));
    EXPECT_TRUE(child->current_frame_host()->GetLastCommittedOrigin().opaque());
    EXPECT_TRUE(
        child->current_frame_host()->GetLastCommittedOrigin().CanBeDerivedFrom(
            main_url));
  }

  {
    GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
    // Navigate the iframe to somewhere else.
    FrameNavigateParamsCapturer capturer(child);
    NavigateFrameToURL(child, url_2);
    capturer.Wait();
  }

  {
    // Remove the sandbox attribute and navigate the iframe back.
    EXPECT_TRUE(ExecJs(
        root, "document.getElementById('child').removeAttribute('sandbox');"));
    FrameNavigateParamsCapturer capturer(child);
    controller.GoBack();
    capturer.Wait();
    // The document is not sandboxed, and has a non-opaque origin based on
    // `main_url`.
    EXPECT_FALSE(child->current_frame_host()->IsSandboxed(
        network::mojom::WebSandboxFlags::kAll));
    EXPECT_FALSE(
        child->current_frame_host()->GetLastCommittedOrigin().opaque());
    EXPECT_EQ(child->current_frame_host()->GetLastCommittedOrigin(),
              url::Origin::Create(main_url));
  }
}

// Tests that navigating to a document that was previously not sandboxed but
// later on is loaded again while being marked as sandboxed would use an opaque
// origin, and the document is marked as sandboxed.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTestNoServer,
                       HistoryNavigationToPreviouslyNonSandboxedDocument) {
  net::test_server::ControllableHttpResponse response1(
      embedded_test_server(), "/sandboxed_on_second_load_only");
  net::test_server::ControllableHttpResponse response2(
      embedded_test_server(), "/sandboxed_on_second_load_only");
  ASSERT_TRUE(embedded_test_server()->Start());

  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  FrameTreeNode* root =
      static_cast<WebContentsImpl*>(contents())->GetPrimaryFrameTree().root();

  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/sandboxed_on_second_load_only"));
  {
    // Navigate to a page that is not sandboxed when it's first loaded.
    TestNavigationObserver nav_observer(contents());
    shell()->LoadURL(main_url);
    response1.WaitForRequest();
    // Note: we use Cache-Control: no-store to prevent the HTTP cache, so that
    // we can change the response on the next navigation to this page.
    response1.Send(
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/html; charset=utf-8\r\n"
        "Cache-Control: no-store\r\n"
        "\r\n"
        "non-sandboxed document");
    response1.Done();
    nav_observer.Wait();
    EXPECT_EQ(main_url, controller.GetLastCommittedEntry()->GetURL());
    // The document is not sandboxed, and should have an origin based on
    // `main_url`.
    EXPECT_FALSE(root->current_frame_host()->IsSandboxed(
        network::mojom::WebSandboxFlags::kAll));
    EXPECT_FALSE(root->current_frame_host()->GetLastCommittedOrigin().opaque());
    EXPECT_EQ(url::Origin::Create(main_url),
              root->current_frame_host()->GetLastCommittedOrigin());
  }

  {
    // Navigate to another document.
    TestNavigationObserver back_load_observer(contents());
    GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
    shell()->LoadURL(url_2);
    back_load_observer.Wait();
    EXPECT_EQ(url_2, controller.GetLastCommittedEntry()->GetURL());
  }

  {
    // Navigate back to a page that was not sandboxed before, but is now
    // sandboxed (as the HTTP response had changed).
    TestNavigationObserver nav_observer(contents());
    controller.GoBack();
    response2.WaitForRequest();
    response2.Send(
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/html; charset=utf-8\r\n"
        "Content-Security-Policy: sandbox\r\n"
        "\r\n"
        "sandboxed document");
    response2.Done();
    nav_observer.Wait();
    EXPECT_EQ(main_url, controller.GetLastCommittedEntry()->GetURL());
    // The document is sandboxed, and has an opaque origin derived from
    // `main_url`.
    // TODO(https://crbug.com/1359351): The origin should be recalculated and
    // result in a non-opaque origin instead.
    EXPECT_TRUE(root->current_frame_host()->IsSandboxed(
        network::mojom::WebSandboxFlags::kAll));
    EXPECT_TRUE(root->current_frame_host()->GetLastCommittedOrigin().opaque());
    EXPECT_TRUE(
        root->current_frame_host()->GetLastCommittedOrigin().CanBeDerivedFrom(
            main_url));
  }
}

// Same with above, but with an iframe and the sandbox attribute instead.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    HistoryNavigationToPreviouslyNonSandboxedDocumentOnIframe) {
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  GURL main_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root =
      static_cast<WebContentsImpl*>(contents())->GetPrimaryFrameTree().root();

  {
    // Create an iframe and navigate it somewhere, to avoid initial empty
    // document replacement on subsequent navigations.
    LoadCommittedCapturer capturer(shell()->web_contents());
    EXPECT_TRUE(ExecJs(root, R"(
        let iframe = document.createElement('iframe');
        iframe.id = 'child';
        iframe.src = location.href;
        document.body.appendChild(iframe);)"));
    capturer.Wait();
  }
  FrameTreeNode* child = root->child_at(0);

  {
    // Navigate the iframe to about:blank.
    FrameNavigateParamsCapturer capturer(child);
    EXPECT_TRUE(ExecJs(root, R"(
        document.getElementById('child').src = 'about:blank';)"));
    capturer.Wait();

    // The document is not sandboxed, and it's origin is the same as the main
    // frame's origin (the initiator origin).
    EXPECT_FALSE(child->current_frame_host()->IsSandboxed(
        network::mojom::WebSandboxFlags::kAll));
    EXPECT_FALSE(
        child->current_frame_host()->GetLastCommittedOrigin().opaque());
    EXPECT_EQ(child->current_frame_host()->GetLastCommittedOrigin(),
              root->current_frame_host()->GetLastCommittedOrigin());
  }

  {
    GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
    // Navigate the iframe to somewhere else.
    FrameNavigateParamsCapturer capturer(child);
    NavigateFrameToURL(child, url_2);
    capturer.Wait();
  }

  {
    // Add the sandbox attribute and navigate the iframe back.
    EXPECT_TRUE(ExecJs(
        root, "document.getElementById('child').setAttribute('sandbox', '');"));
    FrameNavigateParamsCapturer capturer(child);
    controller.GoBack();
    capturer.Wait();
    // The document is sandboxed, and has an opaque origin derived from
    // `main_url` (the initiator origin).
    EXPECT_TRUE(child->current_frame_host()->IsSandboxed(
        network::mojom::WebSandboxFlags::kAll));
    EXPECT_TRUE(child->current_frame_host()->GetLastCommittedOrigin().opaque());
    EXPECT_TRUE(
        child->current_frame_host()->GetLastCommittedOrigin().CanBeDerivedFrom(
            main_url));
  }
}

// Verifies that deletion of speculative RenderFrameHost due to navigation
// cancellation doesn't crash.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    DeleteSpeculativeRenderFrameHostDueToNavigationCancellation_MainFrame) {
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_a(embedded_test_server()->GetURL("a.com", "/title2.html"));
  GURL url_b(embedded_test_server()->GetURL("b.com", "/title3.html"));
  // Navigate to the original page.
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();

  if (ShouldCreateNewHostForAllFrames()) {
    SCOPED_TRACE(testing::Message()
                 << " Testing main frame same-site navigation.");
    // Main frame same-site navigation creates a speculative RenderFrameHost.
    TestNavigationManager navigation_manager(contents(), url_a);
    EXPECT_TRUE(ExecJs(root, JsReplace("location.href = $1;", url_a)));
    EXPECT_TRUE(navigation_manager.WaitForRequestStart());
    EXPECT_TRUE(root->render_manager()->speculative_frame_host());

    // Cancel the navigation. This should trigger the deletion of the
    // speculative RenderFrameHost.
    root->ResetNavigationRequest(NavigationDiscardReason::kCancelled);
    EXPECT_FALSE(root->render_manager()->speculative_frame_host());
  }

  if (AreAllSitesIsolatedForTesting() || ShouldCreateNewHostForAllFrames()) {
    SCOPED_TRACE(testing::Message()
                 << " Testing main frame cross-site navigation.");
    // Main frame cross-site navigation creates a speculative RenderFrameHost.
    TestNavigationManager navigation_manager(contents(), url_b);
    EXPECT_TRUE(ExecJs(root, JsReplace("location.href = $1;", url_b)));
    EXPECT_TRUE(navigation_manager.WaitForRequestStart());
    EXPECT_TRUE(root->render_manager()->speculative_frame_host());
    // Cancel the navigation. This should trigger the deletion of the
    // speculative RenderFrameHost.
    root->ResetNavigationRequest(NavigationDiscardReason::kCancelled);
    EXPECT_FALSE(root->render_manager()->speculative_frame_host());
  }
}

// Same as above but for subframes.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    DeleteSpeculativeRenderFrameHostDueToNavigationCancellation_Subframe) {
  GURL main_url(
      embedded_test_server()->GetURL("a.com", "/page_with_iframe.html"));
  GURL url_a(embedded_test_server()->GetURL("a.com", "/title2.html"));
  GURL url_b(embedded_test_server()->GetURL("b.com", "/title3.html"));
  // Navigate to a page with an iframe.
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  FrameTreeNode* child = root->child_at(0);

  if (ShouldCreateNewHostForSameSiteSubframe()) {
    SCOPED_TRACE(testing::Message()
                 << " Testing subframe same-site navigation.");
    // Subframe same-site navigation creates a speculative RenderFrameHost.
    TestNavigationManager navigation_manager(contents(), url_a);
    EXPECT_TRUE(ExecJs(child, JsReplace("location.href = $1;", url_a)));
    EXPECT_TRUE(navigation_manager.WaitForRequestStart());
    EXPECT_TRUE(child->render_manager()->speculative_frame_host());
    // Cancel the navigation. This should trigger the deletion of the
    // speculative RenderFrameHost.
    child->ResetNavigationRequest(NavigationDiscardReason::kCancelled);
    EXPECT_FALSE(child->render_manager()->speculative_frame_host());
  }

  if (AreAllSitesIsolatedForTesting() ||
      ShouldCreateNewHostForSameSiteSubframe()) {
    SCOPED_TRACE(testing::Message()
                 << " Testing subframe same-site navigation.");
    // Subframe cross-site navigation creates a speculative RenderFrameHost.
    TestNavigationManager navigation_manager(contents(), url_b);
    EXPECT_TRUE(ExecJs(child, JsReplace("location.href = $1;", url_b)));
    EXPECT_TRUE(navigation_manager.WaitForRequestStart());
    EXPECT_TRUE(child->render_manager()->speculative_frame_host());
    child->ResetNavigationRequest(NavigationDiscardReason::kCancelled);

    // Cancel the navigation. This should trigger the deletion of the
    // speculative RenderFrameHost.
    EXPECT_FALSE(child->render_manager()->speculative_frame_host());
  }
}

// Verifies that deletion of speculative RenderFrameHost due to deletion of
// the frame it's committing in doesn't crash.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    DeleteSpeculativeRenderFrameHostDueToFrameDeletion_MainFrame) {
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_a(embedded_test_server()->GetURL("a.com", "/title2.html"));
  GURL url_b(embedded_test_server()->GetURL("b.com", "/title3.html"));
  // Navigate to the original page.
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* original_root = contents()->GetPrimaryFrameTree().root();

  if (ShouldCreateNewHostForAllFrames()) {
    SCOPED_TRACE(testing::Message()
                 << " Testing main frame same-site navigation.");

    // Open a new window.
    ShellAddedObserver new_shell_observer;
    EXPECT_TRUE(ExecJs(original_root,
                       "var w = window.open('" + main_url.spec() + "');"));
    Shell* new_shell = new_shell_observer.GetShell();
    ASSERT_NE(new_shell->web_contents(), shell()->web_contents());
    EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
    FrameTreeNode* new_window_root =
        static_cast<WebContentsImpl*>(new_shell->web_contents())
            ->GetPrimaryFrameTree()
            .root();

    // Main frame same-site navigation in the new window creates a speculative
    // RenderFrameHost.
    TestNavigationManager navigation_manager(new_shell->web_contents(), url_a);
    EXPECT_TRUE(
        ExecJs(new_window_root, JsReplace("location.href = $1;", url_a)));
    EXPECT_TRUE(navigation_manager.WaitForRequestStart());
    EXPECT_TRUE(new_window_root->render_manager()->speculative_frame_host());

    // Close the window. This should trigger the deletion of the
    // speculative RenderFrameHost.
    EXPECT_TRUE(ExecJs(original_root, "w.close()"));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
    EXPECT_FALSE(navigation_manager.was_committed());
  }

  if (AreAllSitesIsolatedForTesting() || ShouldCreateNewHostForAllFrames()) {
    SCOPED_TRACE(testing::Message()
                 << " Testing main frame cross-site navigation.");

    // Open a new window.
    ShellAddedObserver new_shell_observer;
    EXPECT_TRUE(ExecJs(original_root,
                       "var w = window.open('" + main_url.spec() + "');"));
    Shell* new_shell = new_shell_observer.GetShell();
    ASSERT_NE(new_shell->web_contents(), shell()->web_contents());
    EXPECT_TRUE(WaitForLoadStop(new_shell->web_contents()));
    FrameTreeNode* new_window_root =
        static_cast<WebContentsImpl*>(new_shell->web_contents())
            ->GetPrimaryFrameTree()
            .root();

    // Main frame cross-site navigation in the new window creates a speculative
    // RenderFrameHost.
    TestNavigationManager navigation_manager(new_shell->web_contents(), url_b);
    EXPECT_TRUE(
        ExecJs(new_window_root, JsReplace("location.href = $1;", url_b)));
    EXPECT_TRUE(navigation_manager.WaitForRequestStart());
    EXPECT_TRUE(new_window_root->render_manager()->speculative_frame_host());

    // Close the window. This should trigger the deletion of the
    // speculative RenderFrameHost.
    EXPECT_TRUE(ExecJs(original_root, "w.close()"));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
    EXPECT_FALSE(navigation_manager.was_committed());
  }
}

// Same as above but for subframes.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    DeleteSpeculativeRenderFrameHostDueToFrameDeletion_Subframe) {
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_a(embedded_test_server()->GetURL("a.com", "/title2.html"));
  GURL url_b(embedded_test_server()->GetURL("b.com", "/title3.html"));
  // Navigate to the original page.
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();

  if (ShouldCreateNewHostForSameSiteSubframe()) {
    SCOPED_TRACE(testing::Message()
                 << " Testing subframe same-site navigation.");

    {
      // Create a new iframe.
      LoadCommittedCapturer capturer(contents());
      EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, main_url)));
      capturer.Wait();
    }

    // Subframe same-site navigation in the new iframe creates a speculative
    // RenderFrameHost.
    TestNavigationManager navigation_manager(contents(), url_a);
    EXPECT_TRUE(
        ExecJs(root->child_at(0), JsReplace("location.href = $1;", url_a)));
    EXPECT_TRUE(navigation_manager.WaitForRequestStart());
    EXPECT_TRUE(root->child_at(0)->render_manager()->speculative_frame_host());

    // Close the window. This should trigger the deletion of the
    // speculative RenderFrameHost.
    EXPECT_TRUE(
        ExecJs(root, "document.getElementsByTagName('iframe')[0].remove()"));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
    EXPECT_FALSE(navigation_manager.was_committed());
  }

  if (AreAllSitesIsolatedForTesting() ||
      ShouldCreateNewHostForSameSiteSubframe()) {
    SCOPED_TRACE(testing::Message()
                 << " Testing subframe cross-site navigation.");

    {
      // Create a new iframe.
      LoadCommittedCapturer capturer(contents());
      EXPECT_TRUE(ExecJs(root, JsReplace(kAddFrameWithSrcScript, main_url)));
      capturer.Wait();
    }

    // Subframe cross-site navigation in the new iframe creates a speculative
    // RenderFrameHost.
    TestNavigationManager navigation_manager(contents(), url_b);
    EXPECT_TRUE(
        ExecJs(root->child_at(0), JsReplace("location.href = $1;", url_b)));
    EXPECT_TRUE(navigation_manager.WaitForRequestStart());
    EXPECT_TRUE(root->child_at(0)->render_manager()->speculative_frame_host());

    // Close the iframe. This should trigger the deletion of the
    // speculative RenderFrameHost.
    EXPECT_TRUE(
        ExecJs(root, "document.getElementsByTagName('iframe')[0].remove()"));
    ASSERT_TRUE(navigation_manager.WaitForNavigationFinished());
    EXPECT_FALSE(navigation_manager.was_committed());
  }
}

// Test that starting a new navigation will cancel a previous navigation that
// had started earlier if one of the navigations uses a speculative
// RenderFrameHost and the other one does not.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       StartNewNavigationWithExistingNavigation_RequestStart) {
  if (ShouldCreateNewHostForAllFrames()) {
    // This test expects cross-site main frame navigations to change
    // RenderFrameHosts, and same-site main frame navigations to reuse the
    // current RenderFrameHosts.
    return;
  }
  // Ensure same-site navigation won't change RenderFrameHosts due to BFCache.
  DisableBackForwardCacheForTesting(
      contents(), BackForwardCacheImpl::TEST_ASSUMES_NO_RENDER_FRAME_CHANGE);

  GURL url_a1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_a2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  GURL url_a3(embedded_test_server()->GetURL("a.com", "/title3.html"));
  GURL url_b1(embedded_test_server()->GetURL("b.com", "/title1.html"));
  // 1) Navigate to A1.
  EXPECT_TRUE(NavigateToURL(shell(), url_a1));

  // 2) Start a same-RFH navigation to A2, but don't commit it yet.
  TestNavigationManager a2_navigation(shell()->web_contents(), url_a2);
  shell()->LoadURL(url_a2);
  EXPECT_TRUE(a2_navigation.WaitForRequestStart());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_EQ(root->navigation_request(), a2_navigation.GetNavigationHandle());

  // 3) Start a cross-RFH navigation to B1, which will cancel the navigation to
  // A2.
  TestNavigationManager b1_navigation(shell()->web_contents(), url_b1);
  shell()->LoadURL(url_b1);
  EXPECT_TRUE(b1_navigation.WaitForRequestStart());
  EXPECT_EQ(url_b1, b1_navigation.GetNavigationHandle()->GetURL());
  EXPECT_EQ(root->navigation_request(), b1_navigation.GetNavigationHandle());

  // Assert that the navigation to A2 gets cancelled.
  ASSERT_TRUE(a2_navigation.WaitForNavigationFinished());
  EXPECT_FALSE(a2_navigation.was_committed());

  // 4) Start another same-RFH navigation to A3, which will cancel the
  // previous cross-RFH navigation to B1.
  TestNavigationManager a3_navigation(shell()->web_contents(), url_a3);
  shell()->LoadURL(url_a3);
  EXPECT_TRUE(a3_navigation.WaitForRequestStart());
  EXPECT_EQ(url_a3, a3_navigation.GetNavigationHandle()->GetURL());
  EXPECT_EQ(root->navigation_request(), a3_navigation.GetNavigationHandle());

  // Assert that the navigation to B1 gets cancelled.
  ASSERT_TRUE(b1_navigation.WaitForNavigationFinished());
  EXPECT_FALSE(b1_navigation.was_committed());
}

IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    StartNewNavigationWithExistingNavigation_RequestStart_History) {
  if (ShouldCreateNewHostForAllFrames()) {
    // This test expects cross-site main frame navigations to change
    // RenderFrameHosts, and same-site main frame navigations to reuse the
    // current RenderFrameHosts.
    return;
  }
  // Ensure same-site navigation won't change RenderFrameHosts due to BFCache.
  DisableBackForwardCacheForTesting(
      contents(), BackForwardCacheImpl::TEST_ASSUMES_NO_RENDER_FRAME_CHANGE);

  GURL url_a1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_b1(embedded_test_server()->GetURL("b.com", "/title1.html"));
  GURL url_b2(embedded_test_server()->GetURL("b.com", "/title2.html"));
  NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
      shell()->web_contents()->GetController());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // 1) Navigate to A1, then B1, then B2.
  EXPECT_TRUE(NavigateToURL(shell(), url_a1));
  NavigationEntryImpl* entry_a1 = controller.GetLastCommittedEntry();
  SiteInstance* site_instance_a1 =
      root->current_frame_host()->GetSiteInstance();
  EXPECT_EQ(site_instance_a1, entry_a1->site_instance());

  EXPECT_TRUE(NavigateToURL(shell(), url_b1));
  NavigationEntryImpl* entry_b1 = controller.GetLastCommittedEntry();
  SiteInstance* site_instance_b1 =
      root->current_frame_host()->GetSiteInstance();
  EXPECT_EQ(site_instance_b1, entry_b1->site_instance());

  EXPECT_TRUE(NavigateToURL(shell(), url_b2));
  NavigationEntryImpl* entry_b2 = controller.GetLastCommittedEntry();
  SiteInstance* site_instance_b2 =
      root->current_frame_host()->GetSiteInstance();
  EXPECT_EQ(site_instance_b2, entry_b2->site_instance());

  // 2) Go back to trigger a same-RFH history navigation to B1, but don't commit
  // it yet.
  TestNavigationManager b1_navigation(shell()->web_contents(), url_b1);
  controller.GoBack();
  EXPECT_TRUE(b1_navigation.WaitForRequestStart());
  EXPECT_EQ(root->navigation_request(), b1_navigation.GetNavigationHandle());

  // 3) Go back again to trigger a cross-RFH navigation to A1, which will cancel
  // the navigation to B1.
  TestNavigationManager a1_navigation(shell()->web_contents(), url_a1);
  controller.GoBack();
  EXPECT_TRUE(a1_navigation.WaitForRequestStart());
  EXPECT_EQ(url_a1, a1_navigation.GetNavigationHandle()->GetURL());
  EXPECT_EQ(root->navigation_request(), a1_navigation.GetNavigationHandle());

  // Assert that the navigation to B1 gets cancelled.
  ASSERT_TRUE(b1_navigation.WaitForNavigationFinished());
  EXPECT_FALSE(b1_navigation.was_committed());

  // Assert that the navigation to A1 successfully commits.
  ASSERT_TRUE(a1_navigation.WaitForNavigationFinished());
  EXPECT_TRUE(a1_navigation.was_successful());
  // Assert that the correct NavigationEntry is used, and no entry gets
  // corrupted.
  EXPECT_EQ(entry_a1, controller.GetLastCommittedEntry());
  EXPECT_EQ(site_instance_a1, entry_a1->site_instance());
  EXPECT_EQ(site_instance_b1, entry_b1->site_instance());
  EXPECT_EQ(site_instance_b2, entry_b2->site_instance());
}

namespace {
// Starts a navigation to `new_navigation_url` when the navigation mamnaged by
// `prev_navigation_manager` gets to the ReadyToCommit stage.
void StartNavigationOnReadyToCommit(
    Shell* shell,
    TestNavigationManager& prev_navigation_manager,
    GURL& new_navigation_url) {
  base::RunLoop run_loop;
  static_cast<NavigationRequest*>(prev_navigation_manager.GetNavigationHandle())
      ->set_ready_to_commit_callback_for_testing(run_loop.QuitClosure());
  prev_navigation_manager.ResumeNavigation();
  run_loop.Run();
  shell->LoadURL(new_navigation_url);
}
}  // namespace

// Same as StartNewNavigationWithExistingNavigation_RequestStart but start the
// new navigation when the previous navigation is at the "ReadyToCommit" stage,
// after the NavigationRequest's ownership had moved to the committing
// RenderFrameHost. The behavior is a bit different as the cancellation behavior
// differs depending on which navigation starts or commits first.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       StartNewNavigationWithExistingNavigation_ReadyToCommit) {
  if (ShouldCreateNewHostForAllFrames()) {
    // This test expects cross-site main frame navigations to change
    // RenderFrameHosts, and same-site main frame navigations to reuse the
    // current RenderFrameHosts.
    return;
  }
  // Ensure same-site navigation won't change RenderFrameHosts due to BFCache.
  DisableBackForwardCacheForTesting(
      contents(), BackForwardCacheImpl::TEST_ASSUMES_NO_RENDER_FRAME_CHANGE);

  GURL url_a1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_a2(embedded_test_server()->GetURL("a.com", "/title2.html"));
  GURL url_a3(embedded_test_server()->GetURL("a.com", "/title3.html"));
  GURL url_b1(embedded_test_server()->GetURL("b.com", "/title1.html"));
  GURL url_b2(embedded_test_server()->GetURL("b.com", "/title2.html"));
  // 1) Navigate to A1.
  EXPECT_TRUE(NavigateToURL(shell(), url_a1));

  // 2) Start a same-RFH navigation to A2, but don't commit it yet.
  TestNavigationManager a2_navigation(shell()->web_contents(), url_a2);
  shell()->LoadURL(url_a2);
  EXPECT_TRUE(a2_navigation.WaitForResponse());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  EXPECT_EQ(root->navigation_request(), a2_navigation.GetNavigationHandle());

  // 3) Start a cross-RFH navigation to B1 after A2 gets to "pending commit"
  // stage, which will not cancel the navigation to A2 as A2's NavigationRequest
  // had moved.
  TestNavigationManager b1_navigation(shell()->web_contents(), url_b1);
  StartNavigationOnReadyToCommit(shell(), a2_navigation, url_b1);
  EXPECT_TRUE(b1_navigation.WaitForRequestStart());
  EXPECT_EQ(url_b1, b1_navigation.GetNavigationHandle()->GetURL());
  EXPECT_EQ(root->navigation_request(), b1_navigation.GetNavigationHandle());

  // Assert that the navigation to A2 didn't get cancelled, and finish
  // committing A2. This shouldn't cancel the navigation to B1.
  ASSERT_TRUE(a2_navigation.WaitForNavigationFinished());
  EXPECT_TRUE(a2_navigation.was_successful());

  // A2's navigation commit didn't cancel B1's navigation.
  EXPECT_TRUE(b1_navigation.GetNavigationHandle());

  // 4) Start a same-RFH navigation to A3 after B1 gets to "pending commit"
  // stage. The behavior depends on whether the navigation queueing feature
  // level is at least kAvoidRedundantCancellations:
  // - If it is at least kAvoidReundantCancellations, A3's navigation won't
  //   cancel the previous cross-RFH navigation to B1, as B1's NavigationRequest
  //   had moved.
  // - Otherwise, A3's navigation will cancel the previous cross-RFH navigation
  //   to B1, because when a same-RFH navigation starts it will delete the
  //   speculative RFH.
  TestNavigationManager a3_navigation(shell()->web_contents(), url_a3);
  EXPECT_TRUE(b1_navigation.WaitForResponse());
  StartNavigationOnReadyToCommit(shell(), b1_navigation, url_a3);

  if (ShouldAvoidRedundantNavigationCancellations()) {
    // Assert that the navigation to B1 didn't get cancelled, and finish
    // committing B1. This shouldn't cancel the navigation to A3.
    ASSERT_TRUE(b1_navigation.WaitForNavigationFinished());
    EXPECT_TRUE(b1_navigation.was_successful());

    // B1's navigation commit didn't cancel A3's navigation.
    EXPECT_TRUE(a3_navigation.WaitForResponse());
    EXPECT_TRUE(a3_navigation.GetNavigationHandle());
    ASSERT_TRUE(a3_navigation.WaitForNavigationFinished());
    EXPECT_TRUE(a3_navigation.was_successful());
  } else {
    EXPECT_TRUE(a3_navigation.WaitForResponse());
    EXPECT_EQ(url_a3, a3_navigation.GetNavigationHandle()->GetURL());
    EXPECT_EQ(root->navigation_request(), a3_navigation.GetNavigationHandle());

    // Assert that the navigation to B1 gets cancelled.
    EXPECT_TRUE(b1_navigation.WaitForNavigationFinished());
    EXPECT_FALSE(b1_navigation.was_committed());

    // 5) Start a cross-RFH navigation to B2 after A3 gets to "pending commit"
    // stage, which will cancel the previous same-RFH navigation to A3 when B2
    // commits first, because when the previous RFH gets unloaded it will
    // cancel all ongoing navigations in the pending deletion RFH.
    TestNavigationManager b2_navigation(shell()->web_contents(), url_b2);
    // Ignore A3's commit so that B2's navigation can start and finish
    // committing before A3 finishes committing.
    DidCommitNavigationCanceller ignore_a3_commit(
        shell()->web_contents(), url_a3,
        base::BindLambdaForTesting([&]() { shell()->LoadURL(url_b2); }));
    // Continue the A3 navigation, but its commit will be dropped.
    a3_navigation.ResumeNavigation();
    // The navigation to B2 will start, but won't cancel A3's navigation just
    // yet.
    EXPECT_TRUE(b2_navigation.WaitForResponse());
    EXPECT_TRUE(a3_navigation.GetNavigationHandle());

    // The navigation to B2 finished committing, and cancels A3's navigation.
    EXPECT_TRUE(b2_navigation.WaitForNavigationFinished());
    EXPECT_TRUE(b2_navigation.was_successful());
    // Assert A3's navigation finished but didn't get committed.
    EXPECT_TRUE(a3_navigation.WaitForNavigationFinished());
    EXPECT_FALSE(a3_navigation.was_committed());
  }
}

// Tests that calling FrameTreeNode::ResetNavigationRequest() cancels the
// navigation owned by the FrameTreeNode but won't reset a speculative RFH that
// has a pending commit navigation, even if it was used by the deleted
// navigation.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       ResetNavigationRequestWontDeletePendingCommitRFH) {
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_b1(embedded_test_server()->GetURL("b.com", "/title1.html"));
  GURL url_b2(embedded_test_server()->GetURL("b.com", "/title2.html"));
  // Load `main_url`.
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  RenderFrameHostImplWrapper rfh_a(current_main_frame_host());

  // Start the cross-site navigation to `url_b1`. Then, start a cross-site
  // navigation to `url_b2` after the `url_b1` navigation is in the
  // "pending commit" stage, so that both navigations can exist at the same
  // time (the previous NavigationRequest had already been moved to the
  //"pending commit" speculative RFH, and they both use the same speculative
  // RFH).
  TestNavigationManager b1_nav(shell()->web_contents(), url_b1);
  TestNavigationManager b2_nav(shell()->web_contents(), url_b2);
  // Start the navigation to `url_b1` then drop its DidCommit message from the
  // renderer, so that it will stay in the "pending commit" stage for the rest
  // of the test. Then, start a navigation to `url_b2`.
  DidCommitNavigationCanceller ignore_b1_commit(
      shell()->web_contents(), url_b1,
      base::BindLambdaForTesting([&]() { shell()->LoadURL(url_b2); }));
  shell()->LoadURL(url_b1);
  // Assert tht the `url_b1` navigation had started.
  EXPECT_TRUE(b1_nav.WaitForResponse());
  EXPECT_EQ(b1_nav.GetNavigationHandle(), root->navigation_request());

  b1_nav.ResumeNavigation();
  // Assert that the `url_b2` navigation had started, and didn't cancel the
  // `url_b1` navigation.
  EXPECT_TRUE(b2_nav.WaitForRequestStart());
  EXPECT_EQ(b2_nav.GetNavigationHandle(), root->navigation_request());
  EXPECT_TRUE(b1_nav.GetNavigationHandle());

  // Cancel the navigation to `url_b2` by calling ResetNavigationRequest().
  // This shouldn't cancel the navigation to `url_b1`.
  root->ResetNavigationRequest(NavigationDiscardReason::kCancelled);
  ASSERT_TRUE(b2_nav.WaitForNavigationFinished());
  EXPECT_FALSE(b2_nav.was_committed());
  EXPECT_FALSE(b2_nav.GetNavigationHandle());

  // Assert that the `url_b1` navigation and the speculative RenderFrameHost is
  // still around. Note that we can't wait for the navigation to finish here
  // because we've dropped the DidCommit message for this navigation.
  EXPECT_TRUE(b1_nav.GetNavigationHandle());
  EXPECT_TRUE(root->render_manager()->speculative_frame_host());
  EXPECT_EQ(b1_nav.GetNavigationHandle()->GetRenderFrameHost(),
            root->render_manager()->speculative_frame_host());
}

// Tests that when a RenderFrameHost gets into the "pending deletion" state due
// to another RenderFrameHost committing in the same FrameTreeNode, it won't
// cancel other navigations happening in the same FrameTreeNode.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       UnloadingPreviousRFHOnCommitWontCancelNavigation) {
  if (!ShouldAvoidRedundantNavigationCancellations()) {
    return;
  }
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_b1(embedded_test_server()->GetURL("b.com", "/title1.html"));
  GURL url_b2(embedded_test_server()->GetURL("b.com", "/title2.html"));
  // Load `main_url`.
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  RenderFrameHostImplWrapper rfh_a(current_main_frame_host());

  // Start the cross-site navigation to `url_b1`, then start the cross-site
  // navigation to `url_b2` just before the navigation to `url_b1` processes its
  // DidCommitNavigation message, so that both navigation can exist at the same
  // time (the previous NavigationRequest has already been moved to the pending
  // commit speculative RFH, and they both use the same speculative RFH because
  // they use the same SiteInstance).
  TestNavigationManager b1_nav(shell()->web_contents(), url_b1);
  TestNavigationManager b2_nav(shell()->web_contents(), url_b2);
  NavigationStarterBeforeDidCommitNavigation urlb2_navigation_starter(
      contents(), shell(), url_b1, url_b2);
  shell()->LoadURL(url_b1);

  // Check the navigation to `url_b1` started.
  EXPECT_TRUE(b1_nav.WaitForResponse());
  EXPECT_EQ(b1_nav.GetNavigationHandle(), root->navigation_request());
  b1_nav.ResumeNavigation();

  // Check the navigation to `url_b2` started.
  EXPECT_TRUE(b2_nav.WaitForRequestStart());
  EXPECT_EQ(b2_nav.GetNavigationHandle(), root->navigation_request());

  // Wait for the `url_b1` navigation to finish.
  ASSERT_TRUE(b1_nav.WaitForNavigationFinished());
  EXPECT_TRUE(b1_nav.was_successful());

  // The RenderFrameHost had changed, which means we have started to unload the
  // previous RenderFrameHost or saved it in BFCache.
  RenderFrameHostImplWrapper rfh_b(current_main_frame_host());
  EXPECT_NE(rfh_b.get(), rfh_a.get());
  EXPECT_TRUE(!rfh_a.get() || !rfh_a->IsActive());

  // Check that the `url_b2` navigation didn't get cancelled and its associate
  // RFH type is set to NONE.
  EXPECT_NE(nullptr, root->navigation_request());
  EXPECT_EQ(b2_nav.GetNavigationHandle(), root->navigation_request());
  EXPECT_EQ(root->navigation_request()->GetAssociatedRFHType(),
            NavigationRequest::AssociatedRenderFrameHostType::NONE);

  // Check that the `url_b2` navigation's associated RFH type gets updated when
  // it gets its final RenderFrameHost.
  b2_nav.ResumeNavigation();
  EXPECT_TRUE(b2_nav.WaitForResponse());
  if (IsBackForwardCacheEnabled() || ShouldCreateNewHostForAllFrames()) {
    // When BFCache or RenderDocument is enabled, the `url_b2` navigation won't
    // reuse the current RFH.
    EXPECT_EQ(root->navigation_request()->GetAssociatedRFHType(),
              NavigationRequest::AssociatedRenderFrameHostType::SPECULATIVE);
  } else {
    EXPECT_EQ(root->navigation_request()->GetAssociatedRFHType(),
              NavigationRequest::AssociatedRenderFrameHostType::CURRENT);
  }

  // Assert that the `url_b2` navigation committed successfully.
  ASSERT_TRUE(b2_nav.WaitForNavigationFinished());
  EXPECT_TRUE(b2_nav.was_successful());
}

// Test that ConcurrentNavigationsCommitDeferringCondition will queue BFCache
// restore navigations as long as there is a pending commit RenderFrameHost.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       BFCacheRestoreDeferredWhenPendingCommitRFHExists) {
  if (!ShouldAvoidRedundantNavigationCancellations() ||
      !IsBackForwardCacheEnabled()) {
    return;
  }

  GURL url_1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
  GURL url_3(embedded_test_server()->GetURL("c.com", "/title3.html"));

  // Load `url_1`, then `url_2`.
  EXPECT_TRUE(NavigateToURL(shell(), url_1));
  EXPECT_TRUE(NavigateToURL(shell(), url_2));

  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();

  // Start loading `url_3`, but ignore its DidCommit IPC, so that it will stay
  // at the "pending commit" stage indefinitely. Then, start a back navigation
  // to `url_1`, which should try to do a back/forward cache restore.
  TestNavigationManager pending_commit_nav_manager(contents(), url_3);
  TestActivationManager bfcache_nav_manager(contents(), url_1);
  {
    // Note that we need to scope the lifetime of DidCommitNavigationCanceller
    // to avoid crashing during the same-document navigation later on.
    DidCommitNavigationCanceller ignore_url_3_commit(
        contents(), url_3,
        base::BindLambdaForTesting([&]() { controller.GoBack(); }));
    shell()->LoadURL(url_3);
    EXPECT_TRUE(pending_commit_nav_manager.WaitForResponse());
    // Check that a speculative RenderFrameHost was created for the navigation
    // to `url_3`. Continue the `url_3` navigation, which will stay at the
    // "pending commit" stage.
    RenderFrameHostImplWrapper spec_rfh(
        root->render_manager()->speculative_frame_host());
    EXPECT_TRUE(spec_rfh.get());

    pending_commit_nav_manager.ResumeNavigation();

    // Ensure that the BFCache restore navigation starts correctly.
    EXPECT_TRUE(bfcache_nav_manager.WaitForBeforeChecks());

    // The pending commit RenderFrameHost for `url_3` is still around.
    EXPECT_EQ(spec_rfh.get(), root->render_manager()->speculative_frame_host());
    EXPECT_TRUE(spec_rfh->HasPendingCommitForCrossDocumentNavigation());
  }

  // The BFCache restore got queued as there is a pending commit RFH.
  bfcache_nav_manager.ResumeActivation();
  NavigationRequest* bfcache_nav = static_cast<NavigationRequest*>(
      bfcache_nav_manager.GetNavigationHandle());
  EXPECT_TRUE(bfcache_nav->IsQueued());

  // Do a same-document navigation in the renderer, which will create and
  // destruct a NavigationRequest on the browser side. This should not cause the
  // queued BFCache restore to continue, as there is still a pending commit
  // RFH.
  GURL url_2_foo(embedded_test_server()->GetURL("b.com", "/title2.html#foo"));
  EXPECT_TRUE(ExecJs(contents(), "location.href = '#foo'"));
  EXPECT_EQ(url_2_foo, contents()->GetLastCommittedURL());
  EXPECT_TRUE(bfcache_nav->IsQueued());

  // Trigger the pending commit RFH & NavigationRequest deletion, so that the
  // BFCache restore can commit. Note that the pending commit RFH deletion can't
  // actually happen in real life, but this is the best we can do since we've
  // dropped the DidCommit IPC before. Ideally, we would just re-trigger the
  // DidCommit IPC that we dropped, so that the pending commit RFH &
  // NavigationRequest will finish the commit and trigger the resume callback of
  // the queued navigation, but there's currently no way to do that in test.
  // Also, normally we discourage the deletion of pending commit RFHs as that
  // will result in a confused renderer, but in this case the renderer for
  // `url_3` is a brand new renderer that we will never reuse anyways, so it's
  // fine to discard the pending commit RFH.
  // TODO(https://crbug.com/1220337): Use ResumeCommitClosureSetObserver and
  // BeginNavigationInCommitCallbackInterceptor instead of doing this.
  root->render_manager()->DiscardSpeculativeRenderFrameHostForShutdown();
  EXPECT_TRUE(pending_commit_nav_manager.WaitForNavigationFinished());
  EXPECT_FALSE(pending_commit_nav_manager.was_committed());

  // Ensure that the BFCache restore can commit successfully now.
  bfcache_nav_manager.WaitForNavigationFinished();
  EXPECT_TRUE(bfcache_nav_manager.was_successful());
  EXPECT_EQ(url_1, contents()->GetLastCommittedURL());
}

// Same as above, but the BFCached RenderFrameHost gets evicted while it is
// queued, causing the NavigationRequest to not resume after the pending commit
// navigation finished committing.
IN_PROC_BROWSER_TEST_P(
    NavigationControllerBrowserTest,
    BFCacheRestoreDeferredAndEvictedWhenPendingCommitRFHExists) {
  if (!ShouldAvoidRedundantNavigationCancellations() ||
      !IsBackForwardCacheEnabled()) {
    return;
  }

  GURL url_1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
  GURL url_3(embedded_test_server()->GetURL("c.com", "/title3.html"));

  // Load `url_1`, then `url_2`.
  EXPECT_TRUE(NavigateToURL(shell(), url_1));
  NavigationControllerImpl& controller =
      static_cast<NavigationControllerImpl&>(contents()->GetController());
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();
  RenderFrameHostImplWrapper url_1_rfh(root->current_frame_host());

  EXPECT_TRUE(NavigateToURL(shell(), url_2));
  // The previous RenderFrameHost should be BFCached.
  EXPECT_TRUE(url_1_rfh->IsInBackForwardCache());

  // Start loading `url_3`, but ignore its DidCommit IPC, so that it will stay
  // at the "pending commit" stage indefinitely. Then, start a back navigation
  // to `url_1`, which should try to do a back/forward cache restore.
  TestNavigationManager pending_commit_nav_manager(contents(), url_3);
  TestActivationManager bfcache_nav_manager(contents(), url_1);
  {
    DidCommitNavigationCanceller ignore_url_3_commit(
        contents(), url_3,
        base::BindLambdaForTesting([&]() { controller.GoBack(); }));
    shell()->LoadURL(url_3);
    EXPECT_TRUE(pending_commit_nav_manager.WaitForResponse());
    // Check that a speculative RenderFrameHost was created for the navigation
    // to `url_3`. Continue the `url_3` navigation, which will stay at the
    // "pending commit" stage.
    RenderFrameHostImplWrapper spec_rfh(
        root->render_manager()->speculative_frame_host());
    EXPECT_TRUE(spec_rfh.get());

    pending_commit_nav_manager.ResumeNavigation();

    // Ensure that the BFCache restore navigation starts correctly.
    EXPECT_TRUE(bfcache_nav_manager.WaitForBeforeChecks());

    // The pending commit RenderFrameHost for `url_3` is still around.
    EXPECT_EQ(spec_rfh.get(), root->render_manager()->speculative_frame_host());
    EXPECT_TRUE(spec_rfh->HasPendingCommitForCrossDocumentNavigation());
  }

  // The BFCache restore got queued as there is a pending commit RFH.
  bfcache_nav_manager.ResumeActivation();
  NavigationRequest* bfcache_nav = static_cast<NavigationRequest*>(
      bfcache_nav_manager.GetNavigationHandle());
  EXPECT_TRUE(bfcache_nav->IsQueued());

  // Evict the BFCache entry for `url_1_rfh`. This will cause the BFCache
  // restore navigation to be marked for "restart", posting a task to start
  // a new history navigation that won't try to do a BFCache restore.
  DisableBFCacheForRFHForTesting(url_1_rfh.get());

  // Trigger the pending commit RFH & NavigationRequest deletion, so that the
  // BFCache restore can continue. Note that the pending commit RFH deletion
  // can't actually happen in real life, but this is the best we can do since
  // we've dropped the DidCommit IPC before. Ideally, we would just re-trigger
  // the DidCommit IPC that we dropped, so that the pending commit RFH &
  // NavigationRequest will finish the commit and trigger the resume callback of
  // the queued navigation, but there's currently no way to do that in test.
  // Also, normally we discourage the deletion of pending commit RFHs as that
  // will result in a confused renderer, but in this case the renderer for
  // `url_3` is a brand new renderer that we will never reuse anyways, so it's
  // fine to discard the pending commit RFH.
  // TODO(https://crbug.com/1220337): Use ResumeCommitClosureSetObserver and
  // BeginNavigationInCommitCallbackInterceptor instead of doing this.
  TestNavigationManager new_history_navigation_manager(contents(), url_1);
  root->render_manager()->DiscardSpeculativeRenderFrameHostForShutdown();
  EXPECT_TRUE(pending_commit_nav_manager.WaitForNavigationFinished());
  EXPECT_FALSE(pending_commit_nav_manager.was_committed());

  // Ensure that the BFCache restore is still deferred, and eventually gets
  // cancelled, because the corresponding BFCache entry no longer exists.
  EXPECT_TRUE(bfcache_nav->IsQueued());
  bfcache_nav_manager.WaitForNavigationFinished();
  EXPECT_FALSE(bfcache_nav_manager.was_committed());

  // A new history navigation to `url_1` was triggered after the BFCache restore
  // failed, and it commits successfully.
  EXPECT_TRUE(new_history_navigation_manager.WaitForResponse());
  EXPECT_TRUE(new_history_navigation_manager.WaitForNavigationFinished());
  EXPECT_TRUE(new_history_navigation_manager.was_successful());
  EXPECT_EQ(url_1, contents()->GetLastCommittedURL());
}

// Tests that when a WebUI navigation couldn't create a speculative
// RenderFrameHost yet due to navigation queueing, a WebUI object is still
// created successfully and eventually gets moved to the final RenderFrameHost.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
                       WebUICreatedForQueuedNavigation) {
  GURL url_1(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
  GURL url_webui(std::string(kChromeUIScheme) + "://" +
                 std::string(kChromeUIGpuHost));

  // Load `url_1`.
  EXPECT_TRUE(NavigateToURL(shell(), url_1));
  FrameTreeNode* root = contents()->GetPrimaryFrameTree().root();

  // Start loading `url_2`, but ignore its DidCommit IPC, so that it will stay
  // at the "pending commit" stage indefinitely. Then, start a navigation to
  // `url_webui`, which should create WebUI Objects.
  TestNavigationManager pending_commit_nav_manager(contents(), url_2);
  TestNavigationManager webui_nav_manager(contents(), url_webui);
  WebUIImpl* created_webui = nullptr;
  {
    DidCommitNavigationCanceller ignore_url_2_commit(
        contents(), url_2,
        base::BindLambdaForTesting([&]() { shell()->LoadURL(url_webui); }));
    shell()->LoadURL(url_2);
    EXPECT_TRUE(pending_commit_nav_manager.WaitForResponse());

    // Check that a speculative RenderFrameHost was created for the navigation
    // to `url_2`. Continue the `url_2` navigation, which will stay at the
    // "pending commit" stage.
    RenderFrameHostImplWrapper spec_rfh(
        root->render_manager()->speculative_frame_host());
    EXPECT_TRUE(spec_rfh.get());

    pending_commit_nav_manager.ResumeNavigation();

    // Ensure that the WebUI navigation starts correctly and created a WebUI
    // object even though it hasn't created a RenderFrameHost yet.
    EXPECT_TRUE(webui_nav_manager.WaitForRequestStart());
    NavigationRequest* webui_nav = static_cast<NavigationRequest*>(
        webui_nav_manager.GetNavigationHandle());
    EXPECT_FALSE(webui_nav->HasRenderFrameHost());
    EXPECT_TRUE(webui_nav->HasWebUI());
    created_webui = webui_nav->web_ui();
    EXPECT_FALSE(created_webui->HasRenderFrameHost());

    // The pending commit RenderFrameHost for `url_2` is still around and
    // doesn't have a WebUI Object.
    EXPECT_EQ(spec_rfh.get(), root->render_manager()->speculative_frame_host());
    EXPECT_TRUE(spec_rfh->HasPendingCommitForCrossDocumentNavigation());
    EXPECT_NE(webui_nav->dest_site_instance(), spec_rfh->GetSiteInstance());
    EXPECT_FALSE(spec_rfh->web_ui());
  }

  // Trigger the `url_2` pending commit RFH & NavigationRequest deletion, so
  // that the WebUI navigation can continue. Note that the pending commit RFH
  // deletion can't actually happen in real life, but this is the best we can
  // do since we've dropped the DidCommit IPC before. Ideally, we would just
  // re-trigger the DidCommit IPC that we dropped, so that the pending commit
  // RFH & NavigationRequest will finish the commit and trigger the resume
  // callback of the queued navigation, but there's currently no way to do that
  // in test (we have BeginNavigationInCommitCallbackInterceptor but that only
  // triggers renderer-initiated navigations, which can't navigate to WebUI).
  // Also, normally we discourage the deletion of pending commit RFHs as that
  // will result in a confused renderer, but in this case the renderer for
  // `url_2` is a brand new renderer that we will never reuse anyways, so it's
  // fine to discard the pending commit RFH.
  // TODO(https://crbug.com/1220337): Use ResumeCommitClosureSetObserver and
  // BeginNavigationInCommitCallbackInterceptor instead of doing this.
  root->render_manager()->DiscardSpeculativeRenderFrameHostForShutdown();
  EXPECT_TRUE(pending_commit_nav_manager.WaitForNavigationFinished());
  EXPECT_FALSE(pending_commit_nav_manager.was_committed());

  // Thew WebUI navigation continues successfully and the WebUI object
  // successfully moved to the newly created RenderFrameHost.
  webui_nav_manager.ResumeNavigation();
  EXPECT_TRUE(webui_nav_manager.WaitForResponse());
  NavigationRequest* webui_nav =
      static_cast<NavigationRequest*>(webui_nav_manager.GetNavigationHandle());
  EXPECT_TRUE(webui_nav->HasRenderFrameHost());
  EXPECT_FALSE(webui_nav->HasWebUI());
  EXPECT_EQ(created_webui, webui_nav->GetRenderFrameHost()->web_ui());
  EXPECT_TRUE(created_webui->HasRenderFrameHost());
  EXPECT_EQ(created_webui->GetRenderFrameHost(),
            webui_nav->GetRenderFrameHost());
  EXPECT_EQ(webui_nav->GetRenderFrameHost(),
            root->render_manager()->speculative_frame_host());

  // Commit the navigation.
  webui_nav_manager.ResumeNavigation();
  EXPECT_TRUE(webui_nav_manager.WaitForNavigationFinished());
  EXPECT_TRUE(webui_nav_manager.was_successful());
  EXPECT_EQ(url_webui, contents()->GetLastCommittedURL());
  EXPECT_EQ(created_webui, current_main_frame_host()->web_ui());
  EXPECT_EQ(created_webui->GetRenderFrameHost(), current_main_frame_host());
}

INSTANTIATE_TEST_SUITE_P(
    All,
    NavigationControllerAlertDialogBrowserTest,
    testing::Combine(testing::ValuesIn(RenderDocumentFeatureLevelValues()),
                     testing::Bool()),
    NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(
    All,
    NavigationControllerBrowserTest,
    testing::Combine(testing::ValuesIn(RenderDocumentFeatureLevelValues()),
                     testing::Bool()),
    NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(
    All,
    NavigationControllerBrowserTestNoServer,
    testing::Combine(testing::ValuesIn(RenderDocumentFeatureLevelValues()),
                     testing::Bool()),
    NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(
    All,
    NavigationControllerMainDocumentSequenceNumberBrowserTest,
    testing::Combine(testing::ValuesIn(RenderDocumentFeatureLevelValues()),
                     testing::Bool()),
    NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(
    All,
    RequestMonitoringNavigationBrowserTest,
    testing::Combine(testing::ValuesIn(RenderDocumentFeatureLevelValues()),
                     testing::Bool()),
    NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(
    All,
    SandboxedNavigationControllerBrowserTest,
    testing::Combine(testing::ValuesIn(RenderDocumentFeatureLevelValues()),
                     testing::Bool()),
    NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(
    All,
    SandboxedNavigationControllerWithBfcacheBrowserTest,
    testing::Combine(testing::ValuesIn(RenderDocumentFeatureLevelValues()),
                     testing::Values(true)),
    NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(
    All,
    SandboxedNavigationControllerPopupBrowserTest,
    testing::Combine(testing::ValuesIn(RenderDocumentFeatureLevelValues()),
                     testing::Bool()),
    NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(
    All,
    InitialEmptyDocNavigationControllerBrowserTest,
    testing::Combine(testing::ValuesIn(RenderDocumentFeatureLevelValues()),
                     testing::Bool()),
    InitialEmptyDocNavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(
    All,
    LoadDataWithBaseURLWithPossiblyEmptyURLsBrowserTest,
    testing::Combine(testing::ValuesIn(RenderDocumentFeatureLevelValues()),
                     testing::Bool(),
                     testing::Bool(),
                     testing::Bool()),
    LoadDataWithBaseURLWithPossiblyEmptyURLsBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(
    All,
    LoadDataWithBaseURLBrowserTest,
    testing::Combine(testing::ValuesIn(RenderDocumentFeatureLevelValues()),
                     testing::Bool()),
    LoadDataWithBaseURLBrowserTest::DescribeParams);
}  // namespace content