blob: debaf4e2d012ecfa5f56cd6e3cef60d4ca606815 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <stdint.h>
#include <algorithm>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/callback.h"
#include "base/callback_helpers.h"
#include "base/command_line.h"
#include "base/files/scoped_temp_dir.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/sequenced_task_runner.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/sequenced_task_runner_handle.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/renderer_host/display_util.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_entry_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/web_contents/web_contents_impl.h"
#include "content/browser/web_contents/web_contents_view.h"
#include "content/common/content_navigation_policy.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/common/use_zoom_for_dsf_policy.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/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 "content/test/test_content_browser_client.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/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"
namespace content {
namespace {
using testing::ElementsAre;
using testing::IsEmpty;
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() = default;
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());
}
};
class NavigationControllerBrowserTest
: public NavigationControllerBrowserTestBase,
public ::testing::WithParamInterface<std::string> {
public:
NavigationControllerBrowserTest() {
InitAndEnableRenderDocumentFeature(&feature_list_for_render_document_,
GetParam());
}
// Provides meaningful param names instead of /0, /1, ...
static std::string DescribeParams(
const testing::TestParamInfo<ParamType>& info) {
return GetRenderDocumentLevelNameForTestParams(info.param);
}
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()->GetMainFrame()->last_http_method());
return form_post_id;
}
void ReplaceState(FrameTreeNode* node, const std::string& state) {
FrameNavigateParamsCapturer capturer(node);
capturer.set_wait_for_load(false);
EXPECT_TRUE(ExecJs(node, JsReplace("history.replaceState($1, '')", state)));
capturer.Wait();
EXPECT_TRUE(capturer.is_same_document());
}
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 defined(OS_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));
}
// We need to run two versions of these LoadDataWithBaseURL tests, but we
// can't subclass and parameterize NavigationControllerBrowserTest because
// it's already parameterized for RenderDocument.
// TODO(rakina): Once the RenderDocument parameters are removed, change these
// to actual parameterized tests instead.
void RunLoadDataWithInvalidBaseURL(
bool use_load_data_as_string_with_base_url);
void RunLoadDataWithBlockedURL(bool use_load_data_as_string_with_base_url);
void RunLoadDataWithBlockedURLAndInvalidBaseURL(
bool use_load_data_as_string_with_base_url);
void RunLoadDataWithBaseURL(bool use_load_data_as_string_with_base_url,
bool base_url_empty,
bool history_url_empty);
void RunLoadDataWithBaseURLThenJavaScriptURLThenSameDocumentNavigation(
bool use_load_data_as_string_with_base_url);
private:
base::test::ScopedFeatureList feature_list_for_render_document_;
};
// 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::string> {
public:
NavigationControllerBrowserTestNoServer() {
InitAndEnableRenderDocumentFeature(&feature_list_for_render_document_,
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_;
};
// 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())
->GetFrameTree()
->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);
}
// 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.
void NavigationControllerBrowserTest::RunLoadDataWithBaseURL(
bool use_load_data_as_string_with_base_url,
bool base_url_empty,
bool history_url_empty) {
// 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 data = "<html><body>foo</body></html>";
const GURL data_url = GURL(data_header + data);
const GURL base_url = base_url_empty ? GURL() : GURL("http://baseurl");
const GURL history_url =
history_url_empty ? GURL() : GURL("http://historyurl");
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 defined(OS_ANDROID)
shell()->LoadDataAsStringWithBaseURL(history_url, data, base_url);
#else
NOTREACHED();
#endif
} else {
shell()->LoadDataWithBaseURL(history_url, data, base_url);
}
// Verify the pending NavigationEntry.
NavigationEntryImpl* pending_entry = controller.GetPendingEntry();
// The URL of the entry will always be set to the URL used for commit.
EXPECT_EQ(commit_url, pending_entry->GetURL());
// base_url_for_data_url_ will always be set to |base_url|.
// The virtual URL will be |history_url|, unless it's empty, in which case
// we'll fall back to the URL used for the commit.
GURL virtual_url = history_url_empty ? commit_url : history_url;
EXPECT_EQ(virtual_url, pending_entry->GetVirtualURL());
// The history URL in the NavigationEntry will always be set to the virtual
// URL, unless the base URL is empty.
GURL history_url_for_data_url = base_url_empty ? GURL() : virtual_url;
EXPECT_EQ(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.
NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
EXPECT_EQ(commit_url, entry->GetURL());
EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
EXPECT_EQ(virtual_url, entry->GetVirtualURL());
EXPECT_EQ(history_url_for_data_url, entry->GetHistoryURLForDataURL());
// We should use commit_url instead of the base_url as the original url of
// this navigation entry, because base_url is only used for resolving relative
// paths in the data, or enforcing same origin policy.
EXPECT_EQ(commit_url, entry->GetOriginalRequestURL());
// data: URL loads always have HTTP status code 200.
EXPECT_EQ(200, contents()->GetMainFrame()->last_http_status_code());
// Verify that the page is not classified as an error page.
EXPECT_EQ(PAGE_TYPE_NORMAL, entry->GetPageType());
EXPECT_FALSE(contents()->GetMainFrame()->is_error_page());
// The redirect chain contains the base URL instead of the commit URL or
// the history URL, because it's the URL used by the DocumentLoader (unless
// the base URL is empty).
// TODO(https://crbug.com/1171237): Should we use the commit/history URL
// instead?
EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
GURL url_in_redirect_chain = base_url_empty ? commit_url : base_url;
EXPECT_EQ(entry->GetRedirectChain()[0], url_in_redirect_chain);
// The original request URL for loadDataWithBaseURL navigations will be the
// URL used for commit (the data URL/header).
EXPECT_EQ(entry->GetOriginalRequestURL(), commit_url);
// 3) Now reload and make sure the renderer isn't killed.
ReloadBlockUntilNavigationsComplete(shell(), 1);
EXPECT_TRUE(shell()->web_contents()->GetMainFrame()->IsRenderFrameLive());
// Verify the last committed NavigationEntry hasn't changed.
NavigationEntryImpl* reload_entry = controller.GetLastCommittedEntry();
EXPECT_EQ(entry, reload_entry);
EXPECT_EQ(commit_url, reload_entry->GetURL());
EXPECT_EQ(base_url, reload_entry->GetBaseURLForDataURL());
EXPECT_EQ(virtual_url, reload_entry->GetVirtualURL());
EXPECT_EQ(history_url_for_data_url, reload_entry->GetHistoryURLForDataURL());
EXPECT_EQ(commit_url, reload_entry->GetOriginalRequestURL());
EXPECT_EQ(reload_entry->GetRedirectChain().size(), 1u);
EXPECT_EQ(reload_entry->GetRedirectChain()[0], url_in_redirect_chain);
// The original request URL for loadDataWithBaseURL navigations will be the
// URL used for commit (the data URL/header).
EXPECT_EQ(entry->GetOriginalRequestURL(), commit_url);
}
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, LoadDataWithBaseURL) {
RunLoadDataWithBaseURL(false /* use_load_data_as_string_with_base_url */,
false /* base_url_empty */,
false /* history_url_empty */);
}
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
LoadDataWithEmptyBaseURL) {
RunLoadDataWithBaseURL(false /* use_load_data_as_string_with_base_url */,
true /* base_url_empty */,
false /* history_url_empty */);
}
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
LoadDataWithEmptyHistoryURL) {
RunLoadDataWithBaseURL(false /* use_load_data_as_string_with_base_url */,
false /* base_url_empty */,
true /* history_url_empty */);
}
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
LoadDataWithEmptyBaseAndHistoryURL) {
RunLoadDataWithBaseURL(false /* use_load_data_as_string_with_base_url */,
true /* base_url_empty */,
true /* history_url_empty */);
}
#if defined(OS_ANDROID)
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
LoadDataAsStringWithBaseURL) {
RunLoadDataWithBaseURL(true /* use_load_data_as_string_with_base_url */,
false /* base_url_empty */,
false /* history_url_empty */);
}
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
LoadDataAsStringWithEmptyBaseURL) {
RunLoadDataWithBaseURL(true /* use_load_data_as_string_with_base_url */,
true /* base_url_empty */,
false /* history_url_empty */);
}
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
LoadDataAsStringWithEmptyHistoryRL) {
RunLoadDataWithBaseURL(true /* use_load_data_as_string_with_base_url */,
false /* base_url_empty */,
true /* history_url_empty */);
}
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
LoadDataAsStringWithEmptyBaseAndHistoryURL) {
RunLoadDataWithBaseURL(true /* use_load_data_as_string_with_base_url */,
true /* base_url_empty */,
true /* history_url_empty */);
}
#endif
// 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()->GetMainFrame()->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()->GetMainFrame()->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()->GetMainFrame()->GetLastCommittedURL());
EXPECT_EQ(url::Origin::Create(base_url),
shell()->web_contents()->GetMainFrame()->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()->GetMainFrame()->IsRenderFrameLive());
}
// Tests that navigating with LoadDataWithBaseURL succeeds even when the base
// URL given is invalid.
// Note that this is a function that is called from the actual tests below,
// essentially doing manual parameterization because we can't subclass and add
// more parameters to the already parameterized NavigationControllerBrowserTest.
void NavigationControllerBrowserTest::RunLoadDataWithInvalidBaseURL(
bool use_load_data_as_string_with_base_url) {
// 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());
{
// 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());
// Verify that the page is not classified as an error page.
EXPECT_EQ(PAGE_TYPE_NORMAL, entry->GetPageType());
EXPECT_FALSE(contents()->GetMainFrame()->is_error_page());
const GURL push_state_url =
GURL("data:text/html;charset=utf-8," + data + "#foo");
EXPECT_EQ(
use_load_data_as_string_with_base_url ? history_url : push_state_url,
contents()->GetMainFrame()->GetLastCommittedURL());
}
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
LoadDataWithInvalidBaseURL) {
RunLoadDataWithInvalidBaseURL(
false /* use_load_data_as_string_with_base_url */);
}
#if defined(OS_ANDROID)
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
LoadDataAsStringWithInvalidBaseURL) {
RunLoadDataWithInvalidBaseURL(
true /* use_load_data_as_string_with_base_url */);
}
#endif
// ContentBrowserClient that blocks normal commits to any URL in
// VerifyDidCommitParams.
class BlockAllCommitContentBrowserClient : public TestContentBrowserClient {
public:
// Any visit to any URL will be blocked by VerifyDidCommitParams, except if
// the checks are skipped (e.g. loadDataWithBaseURL).
BlockAllCommitContentBrowserClient() = default;
bool CanCommitURL(RenderProcessHost* process_host,
const GURL& site_url) override {
return false;
}
private:
DISALLOW_COPY_AND_ASSIGN(BlockAllCommitContentBrowserClient);
};
// Tests that navigating with LoadDataWithBaseURL succeeds even when the data
// URL is typically blocked by an embedder.
// Note that this is a function that is called from the actual tests below,
// essentially doing manual parameterization because we can't subclass and add
// more parameters to the already parameterized NavigationControllerBrowserTest.
void NavigationControllerBrowserTest::RunLoadDataWithBlockedURL(
bool use_load_data_as_string_with_base_url) {
// 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;
ContentBrowserClient* old_client =
SetBrowserClientForTesting(&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()->GetMainFrame()->GetLastCommittedURL());
// Verify that the page is not classified as an error page.
EXPECT_EQ(PAGE_TYPE_NORMAL, entry->GetPageType());
EXPECT_FALSE(contents()->GetMainFrame()->is_error_page());
{
// 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()->GetMainFrame()->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()->GetMainFrame()->GetLastCommittedURL());
// Verify that the page is not classified as an error page.
EXPECT_EQ(PAGE_TYPE_NORMAL, entry->GetPageType());
EXPECT_FALSE(contents()->GetMainFrame()->is_error_page());
{
// 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()->GetMainFrame()->GetLastCommittedURL());
// Verify that the page is not classified as an error page.
EXPECT_EQ(PAGE_TYPE_NORMAL, entry->GetPageType());
EXPECT_FALSE(contents()->GetMainFrame()->is_error_page());
SetBrowserClientForTesting(old_client);
}
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
LoadDataWithBlockedURL) {
RunLoadDataWithBlockedURL(false /* use_load_data_as_string_with_base_url */);
}
#if defined(OS_ANDROID)
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
LoadDataAsStringWithBlockedURL) {
RunLoadDataWithBlockedURL(true /* use_load_data_as_string_with_base_url */);
}
#endif
// Tests that same-document navigations after a LoadDataWithBaseURL to a blocked
// URL and an invalid base_url succeeds.
// Note that this is a function that is called from the actual tests below,
// essentially doing manual parameterization because we can't subclass and add
// more parameters to the already parameterized NavigationControllerBrowserTest.
void NavigationControllerBrowserTest::
RunLoadDataWithBlockedURLAndInvalidBaseURL(
bool use_load_data_as_string_with_base_url) {
// 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());
const GURL data_url = GURL("data:text/html;charset=utf-8," + data);
BlockAllCommitContentBrowserClient content_browser_client;
ContentBrowserClient* old_client =
SetBrowserClientForTesting(&content_browser_client);
LoadDataWithBaseURL(base_url, data, history_url, title,
use_load_data_as_string_with_base_url);
// The navigation succeeds even though the base URL is invalid.
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
NavigationEntryImpl* entry = controller.GetLastCommittedEntry();
EXPECT_EQ(1, controller.GetEntryCount());
EXPECT_EQ(base_url, entry->GetBaseURLForDataURL());
EXPECT_EQ(use_load_data_as_string_with_base_url ? history_url : data_url,
contents()->GetMainFrame()->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(use_load_data_as_string_with_base_url ? history_url.spec()
: (data_url.spec() + "#foo"),
contents()->GetMainFrame()->GetLastCommittedURL().spec());
SetBrowserClientForTesting(old_client);
}
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
LoadDataWithBlockedURLAndInvalidBaseURL) {
RunLoadDataWithBlockedURLAndInvalidBaseURL(
false /* use_load_data_as_string_with_base_url */);
}
#if defined(OS_ANDROID)
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
LoadDataAsStringWithBlockedURLAndInvalidBaseURL) {
RunLoadDataWithBlockedURLAndInvalidBaseURL(
true /* use_load_data_as_string_with_base_url */);
}
#endif
// 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.
// Note that this is a function that is called from the actual tests below,
// essentially doing manual parameterization because we can't subclass and add
// more parameters to the already parameterized NavigationControllerBrowserTest.
void NavigationControllerBrowserTest::
RunLoadDataWithBaseURLThenJavaScriptURLThenSameDocumentNavigation(
bool use_load_data_as_string_with_base_url) {
// 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()->GetMainFrame()->GetLastCommittedURL());
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->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()->GetMainFrame()->GetLastCommittedURL());
EXPECT_EQ(start_dsn, entry->GetFrameEntry(root)->document_sequence_number());
}
IN_PROC_BROWSER_TEST_P(
NavigationControllerBrowserTest,
LoadDataWithBaseURLThenJavaScriptURLThenSameDocumentNavigation) {
RunLoadDataWithBaseURLThenJavaScriptURLThenSameDocumentNavigation(
false /* use_load_data_as_string_with_base_url */);
}
#if defined(OS_ANDROID)
IN_PROC_BROWSER_TEST_P(
NavigationControllerBrowserTest,
LoadDataAsStringWithBaseURLThenJavaScriptURLThenSameDocumentNavigation) {
RunLoadDataWithBaseURLThenJavaScriptURLThenSameDocumentNavigation(
true /* use_load_data_as_string_with_base_url */);
}
#endif
// 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()->GetMainFrame()->is_error_page());
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 defined(OS_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())
->GetFrameTree()
->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 more.
for (int url_index = kMaxEntryCount; url_index < kMaxEntryCount + 2;
++url_index) {
GURL url(base::StringPrintf("data:text/html,page%d", url_index));
EXPECT_TRUE(NavigateToURL(shell(), url));
}
// We expect page0 and page1 to be gone.
EXPECT_EQ(kMaxEntryCount, controller.GetEntryCount());
EXPECT_EQ(GURL("data:text/html,page2"),
controller.GetEntryAtIndex(0)->GetURL());
// Now try to go back. This should not hang.
ASSERT_TRUE(controller.CanGoBack());
controller.GoBack();
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// This should have successfully gone back.
EXPECT_EQ(GURL(base::StringPrintf("data:text/html,page%d", kMaxEntryCount)),
controller.GetLastCommittedEntry()->GetURL());
}
namespace {
// 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();
NavigationControllerImpl& controller =
static_cast<NavigationControllerImpl&>(web_contents->GetController());
WaitForLoadStop(web_contents);
TestNavigationManager navigation_manager(web_contents, url);
const GURL& current_url = web_contents->GetMainFrame()->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());
// Both should_replace_entry (in the pending NavigationEntry) and
// should_replace_current_entry (in NavigationRequest params) should be
// true.
EXPECT_TRUE(controller.GetPendingEntry()->should_replace_entry());
EXPECT_TRUE(
NavigationRequest::From(navigation_manager.GetNavigationHandle())
->common_params()
.should_replace_current_entry);
}
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()->GetMainFrame()->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 defined(OS_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())
->GetFrameTree()
->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(
node->current_frame_host()->delegate()->GetAsWebContents()),
frame_tree_node_id_(node->frame_tree_node_id()),
message_loop_runner_(new MessageLoopRunner) {}
// Observes the load commit for the next created frame in the specified
// |web_contents|.
explicit LoadCommittedCapturer(WebContents* web_contents)
: WebContentsObserver(web_contents),
frame_tree_node_id_(0),
message_loop_runner_(new MessageLoopRunner) {}
void Wait() { message_loop_runner_->Run(); }
ui::PageTransition transition_type() const { return transition_type_; }
private:
void RenderFrameCreated(RenderFrameHost* render_frame_host) override {
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 causes a
// subframe navigation without having any committed entry. Such navigations
// just get thrown on the ground, but we shouldn't crash.
//
// This test actually hits NAVIGATION_TYPE_NAV_IGNORE four times. Two of them,
// the initial window.open() and the iframe creation, don't try to create
// navigation entries, and the third and fourth, the new navigations, try to.
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 window with no last committed entry.
Shell* new_shell = OpenBlankWindow(contents());
FrameTreeNode* new_root =
static_cast<WebContentsImpl*>(new_shell->web_contents())
->GetFrameTree()
->root();
// Make a new iframe in it.
NoNavigationsObserver observer(new_shell->web_contents());
{
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));
// Navigate it cross-site.
GURL frame_url = embedded_test_server()->GetURL(
"foo.com", "/navigation_controller/simple_page_2.html");
{
LoadCommittedCapturer capturer(new_shell->web_contents());
std::string script = JsReplace("location.assign($1);", frame_url);
EXPECT_TRUE(ExecJs(new_root->child_at(0), script));
capturer.Wait();
}
// Success is not crashing, and not navigating.
EXPECT_EQ(nullptr,
new_shell->web_contents()->GetController().GetLastCommittedEntry());
// 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());
}
// 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(
"/navigation_controller/simple_page_1.html");
EXPECT_TRUE(NavigateToURL(shell(), url1));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
// Pop open a new window to about:blank.
ShellAddedObserver new_shell_observer;
EXPECT_TRUE(ExecJs(root, "var w = window.open('about:blank')"));
Shell* new_shell = new_shell_observer.GetShell();
ASSERT_NE(new_shell->web_contents(), shell()->web_contents());
FrameTreeNode* new_root =
static_cast<WebContentsImpl*>(new_shell->web_contents())
->GetFrameTree()
->root();
GURL blank_url(url::kAboutBlankURL);
EXPECT_EQ(blank_url, new_root->current_url());
// Make a new iframe in it using document.write from the opener.
{
LoadCommittedCapturer capturer(new_shell->web_contents());
std::string html = "<iframe src='" + url1.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());
EXPECT_EQ(blank_url, new_root->current_url());
EXPECT_EQ(url1, new_root->child_at(0)->current_url());
// Navigate the subframe.
GURL url2 = embedded_test_server()->GetURL(
"/navigation_controller/simple_page_2.html");
{
LoadCommittedCapturer capturer(new_root->child_at(0));
std::string script = "location.href = '" + url2.spec() + "';";
EXPECT_TRUE(ExecJs(new_root->child_at(0), script));
capturer.Wait();
}
EXPECT_EQ(blank_url, new_root->current_url());
EXPECT_EQ(url2, new_root->child_at(0)->current_url());
EXPECT_EQ(2, new_shell->web_contents()->GetController().GetEntryCount());
// Do a replace state in the main frame, which changes the URL from
// about:blank to the opener's origin, due to the document.write() call.
{
LoadCommittedCapturer capturer(new_root);
std::string script = "history.replaceState({}, 'foo', 'foo');";
EXPECT_TRUE(ExecJs(new_root, script));
capturer.Wait();
}
EXPECT_EQ(embedded_test_server()->GetURL("/navigation_controller/foo"),
new_root->current_url());
EXPECT_EQ(url2, new_root->child_at(0)->current_url());
// Go back in the subframe. Note that the main frame's URL looks like a
// cross-origin change from a web URL to about:blank.
{
TestNavigationObserver observer(new_shell->web_contents(), 1);
new_shell->web_contents()->GetController().GoBack();
observer.Wait();
}
EXPECT_TRUE(new_root->current_frame_host()->IsRenderFrameLive());
// Go forward in the subframe. Note that the main frame's URL looks like a
// cross-origin change from about:blank to a web URL.
{
TestNavigationObserver observer(new_shell->web_contents(), 1);
new_shell->web_contents()->GetController().GoForward();
observer.Wait();
}
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()->GetFrameTree()->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 URL
// in renderer" 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_url_in_renderer());
EXPECT_EQ(second_frame_url, child1->GetLastCommittedURL());
EXPECT_EQ(second_frame_url, child1->last_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()->GetFrameTree()->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 URL
// in renderer" is updated correctly to "about:blank".
EXPECT_EQ(frame_url, child0->GetLastCommittedURL());
EXPECT_EQ(GURL(url::kAboutBlankURL), child0->last_url_in_renderer());
EXPECT_EQ(GURL(url::kAboutBlankURL), child1->GetLastCommittedURL());
EXPECT_EQ(GURL(url::kAboutBlankURL), child1->last_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()->GetFrameTree()->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 URL
// in renderer" is updated correctly to "about:srdoc".
EXPECT_EQ(frame_url, child0->GetLastCommittedURL());
EXPECT_EQ(GURL("about:srcdoc"), child0->last_url_in_renderer());
EXPECT_EQ(GURL("about:srcdoc"), child1->GetLastCommittedURL());
EXPECT_EQ(GURL("about:srcdoc"), child1->last_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()->GetFrameTree()->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 URL
// in renderer" is updated correctly to the blob URL.
EXPECT_EQ(frame_url, child0->GetLastCommittedURL());
EXPECT_TRUE(child0->last_url_in_renderer().SchemeIsBlob());
EXPECT_TRUE(child1->GetLastCommittedURL().SchemeIsBlob());
EXPECT_TRUE(child1->last_url_in_renderer().SchemeIsBlob());
EXPECT_EQ(child0->last_url_in_renderer(), child1->last_url_in_renderer());
}
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, ErrorPageReplacement) {
NavigationController& controller = shell()->web_contents()->GetController();
GURL error_url = embedded_test_server()->GetURL("/close-socket");
GetIOThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&net::URLRequestFailedJob::AddUrlHandler));
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
EXPECT_EQ(1, controller.GetEntryCount());
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
// 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.
{
FrameNavigateParamsCapturer capturer(root);
NavigateFrameToURL(root, error_url);
capturer.Wait();
EXPECT_EQ(NAVIGATION_TYPE_NEW_ENTRY, capturer.navigation_type());
NavigationEntry* entry = controller.GetLastCommittedEntry();
EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
EXPECT_EQ(2, controller.GetEntryCount());
}
// Navigate again to the page that fails to load. It results in an error page,
// the NEW_ENTRY navigation type with replacement, and no addition to the
// history list.
{
FrameNavigateParamsCapturer capturer(root);
NavigateFrameToURL(root, error_url);
capturer.Wait();
EXPECT_EQ(NAVIGATION_TYPE_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());
}
// Make a new entry ...
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
EXPECT_EQ(3, controller.GetEntryCount());
// ... and replace it with a failed load.
{
FrameNavigateParamsCapturer capturer(root);
RendererLocationReplace(shell(), error_url);
capturer.Wait();
EXPECT_EQ(NAVIGATION_TYPE_NEW_ENTRY, capturer.navigation_type());
EXPECT_TRUE(capturer.did_replace_entry());
NavigationEntry* entry = controller.GetLastCommittedEntry();
EXPECT_EQ(PAGE_TYPE_ERROR, entry->GetPageType());
EXPECT_EQ(3, controller.GetEntryCount());
}
// 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(4, controller.GetEntryCount());
// ... 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_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());
}
}
// 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_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())
->GetFrameTree()
->root();
{
// 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_NEW_ENTRY, capturer.navigation_type());
EXPECT_FALSE(capturer.is_same_document());
}
{
// 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_NEW_ENTRY, capturer.navigation_type());
EXPECT_TRUE(capturer.is_same_document());
}
{
// 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_NEW_ENTRY, capturer.navigation_type());
EXPECT_FALSE(capturer.is_same_document());
}
{
// 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::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
ui::PAGE_TRANSITION_CLIENT_REDIRECT)));
EXPECT_EQ(NAVIGATION_TYPE_NEW_ENTRY, capturer.navigation_type());
EXPECT_FALSE(capturer.is_same_document());
}
{
// 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::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
ui::PAGE_TRANSITION_CLIENT_REDIRECT)));
EXPECT_EQ(NAVIGATION_TYPE_NEW_ENTRY, capturer.navigation_type());
EXPECT_TRUE(capturer.is_same_document());
}
// 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_NEW_ENTRY, capturer.navigation_type());
EXPECT_TRUE(capturer.did_replace_entry());
EXPECT_FALSE(capturer.is_same_document());
}
// Verify that navigations for NAVIGATION_TYPE_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())
->GetFrameTree()
->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_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_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_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_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_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_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_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()->GetMainFrame()->Reload();
capturer.Wait();
// We're classifying this as EXISTING_ENTRY.
EXPECT_EQ(NAVIGATION_TYPE_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_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_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_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_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_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.
{
// 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)));
EXPECT_EQ(NAVIGATION_TYPE_EXISTING_ENTRY, capturer.navigation_type());
EXPECT_TRUE(capturer.did_replace_entry());
EXPECT_TRUE(capturer.is_same_document());
}
{
// 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)));
EXPECT_EQ(NAVIGATION_TYPE_EXISTING_ENTRY, capturer.navigation_type());
EXPECT_TRUE(capturer.is_same_document());
}
// 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_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_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_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_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())
->GetFrameTree()
->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_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_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/#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_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();
}
{
// Navigate to the same URL (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;
}));
FrameNavigateParamsCapturer capturer(root);
EXPECT_FALSE(NavigateToURL(shell(), url1));
capturer.Wait();
// We're classifying this as NEW_ENTRY.
EXPECT_EQ(NAVIGATION_TYPE_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(1, controller.GetEntryCount());
url_loader_interceptor.reset();
previous_entry = controller.GetLastCommittedEntry();
}
{
// 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_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_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.
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);
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_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 (if it becomes a reload) or NEW_ENTRY (if it becomes a
// replacement navigation).
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())
->GetFrameTree()
->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_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), but should still behave like a renderer-initiated
// navigation, which will do a replacement instead of reload for same-URL
// navigations.
FrameNavigateParamsCapturer capturer(root);
EXPECT_TRUE(ExecJs(contents(), JsReplace("location.href = $1", web_ui_url),
EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */));
capturer.Wait();
// We're classifying this as NEW_ENTRY.
EXPECT_EQ(NAVIGATION_TYPE_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/#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());
}
}
// 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())
->GetFrameTree()
->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_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_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 required when ZoomForDSFEnabled 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 (IsUseZoomForDSFEnabled() && !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 required when ZoomForDSFEnabled 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 (IsUseZoomForDSFEnabled() && !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())
->GetFrameTree()
->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_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())
->GetFrameTree()
->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())
->GetFrameTree()
->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) {
std::string render_document_level;
bool renderer_initiated;
std::tie(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 checks if its navigation type is
// NAVIGATION_TYPE_NEW_ENTRY 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 NavigateWindowAndCheckNavigationTypeIsNewEntry(
WebContentsImpl* web_contents,
const GURL& url,
bool wait_for_previous_navigations = true,
bool expect_same_document = false) {
FrameTreeNode* root = web_contents->GetFrameTree()->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(NAVIGATION_TYPE_NEW_ENTRY, capturer.navigation_type());
EXPECT_FALSE(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 hung_url(embedded_test_server()->GetURL("/hung"));
EXPECT_TRUE(NavigateToURL(shell(), url_1));
FrameTreeNode* root =
static_cast<WebContentsImpl*>(contents())->GetFrameTree()->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 */);
// 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(), root->child_at(subframe_index), "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());
// Do a navigation on the "child1" subframe to about:blank#foo, creating a
// same-document navigation. If it's a renderer-initiated navigation, the
// navigation will be classified as "auto", so we won't append a new
// NavigationEntry, and instead update the current NavigationEntry. However,
// if it's a browser-initiated navigation, the navigation is classified as
// "new" and will create a new entry instead.
// TODO(rakina): Make the browser-initiated and renderer-initiated
// navigation case have the same behavior, once the discussion in
// https://github.com/whatwg/html/issues/6491 converges.
NavigateSubframeAndCheckNavigationType(
contents(), root->child_at(subframe_index), "child2",
GURL("about:blank#foo"),
renderer_initiated() ? NAVIGATION_TYPE_AUTO_SUBFRAME
: NAVIGATION_TYPE_NEW_SUBFRAME);
if (!renderer_initiated())
expected_entry_count++;
EXPECT_EQ(expected_entry_count, controller.GetEntryCount());
// Do a navigation on the "child2" subframe to |url_2|.
NavigateSubframeAndCheckNavigationType(
contents(), root->child_at(subframe_index), "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());
}
// 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());
// 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(), root->child_at(subframe_index), "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.
{
SCOPED_TRACE(testing::Message() << " Testing case 4.");
// Create the "child4" subframe with src set to a URL that never commits.
CreateSubframe(contents(), "child4", hung_url,
false /* wait_for_navigation */);
subframe_index++;
EXPECT_EQ(expected_entry_count, controller.GetEntryCount());
// 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(), root->child_at(subframe_index), "child4", url_2,
NAVIGATION_TYPE_AUTO_SUBFRAME);
EXPECT_EQ(expected_entry_count, controller.GetEntryCount());
}
// 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());
EXPECT_EQ(GURL("about:blank"),
root->child_at(subframe_index)->current_url());
// The frame should be on its initial empty document.
EXPECT_FALSE(root->child_at(subframe_index)->has_committed_real_load());
EXPECT_TRUE(
root->child_at(subframe_index)
->is_on_initial_empty_document_or_subsequent_empty_documents());
// 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(root->child_at(subframe_index), "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"), root->child_at(subframe_index)
->current_frame_host()
->GetLastCommittedURL());
EXPECT_EQ(url_1, root->child_at(subframe_index)
->current_frame_host()
->last_url_in_renderer());
// The frame lost its "initial empty document" status, but
// `has_committed_real_load` is still false because the last committed URL
// stays the same.
EXPECT_FALSE(root->child_at(subframe_index)->has_committed_real_load());
EXPECT_FALSE(
root->child_at(subframe_index)
->is_on_initial_empty_document_or_subsequent_empty_documents());
// 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(), root->child_at(subframe_index), "child5", url_2,
NAVIGATION_TYPE_NEW_SUBFRAME);
expected_entry_count++;
EXPECT_EQ(expected_entry_count, controller.GetEntryCount());
}
// 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.
CreateSubframe(contents(), "child6", GURL("javascript:'foo'"),
false /* wait_for_navigation */);
subframe_index++;
EXPECT_EQ(expected_entry_count, controller.GetEntryCount());
// 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(), root->child_at(subframe_index), "child6", url_2,
NAVIGATION_TYPE_AUTO_SUBFRAME);
EXPECT_EQ(expected_entry_count, controller.GetEntryCount());
}
}
// 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 that won't create a NavigationEntry.
Shell* new_shell = OpenBlankWindow(contents());
WebContentsImpl* new_contents =
static_cast<WebContentsImpl*>(new_shell->web_contents());
NavigationControllerImpl& controller = new_contents->GetController();
EXPECT_EQ(0, controller.GetEntryCount());
EXPECT_FALSE(controller.GetLastCommittedEntry());
// Navigating the window to |url_2| will be classified as NEW_ENTRY and will
// add a new entry.
NavigateWindowAndCheckNavigationTypeIsNewEntry(new_contents, url_2);
EXPECT_EQ(1, controller.GetEntryCount());
EXPECT_TRUE(controller.GetLastCommittedEntry());
}
// 2) Navigate to about:blank on a new window that hasn't done any navigation.
// This case is not enabled for browser-initiated navigation because the
// browser-calculated vs renderer-calculated origin doesn't match, leading to
// a crash.
{
SCOPED_TRACE(testing::Message() << " Testing case 2.");
// Create a new blank window that won't create a NavigationEntry.
Shell* new_shell = OpenBlankWindow(contents());
WebContentsImpl* new_contents =
static_cast<WebContentsImpl*>(new_shell->web_contents());
NavigationControllerImpl& controller = new_contents->GetController();
EXPECT_EQ(0, controller.GetEntryCount());
EXPECT_FALSE(controller.GetLastCommittedEntry());
// Navigating the window to about:blank will be classified as NEW_ENTRY
// and will add a new entry.
NavigateWindowAndCheckNavigationTypeIsNewEntry(new_contents,
GURL("about:blank"));
EXPECT_EQ(1, controller.GetEntryCount());
EXPECT_TRUE(controller.GetLastCommittedEntry());
}
// 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 that won't create a NavigationEntry.
Shell* new_shell = OpenBlankWindow(contents());
WebContentsImpl* new_contents =
static_cast<WebContentsImpl*>(new_shell->web_contents());
NavigationControllerImpl& controller = new_contents->GetController();
EXPECT_EQ(0, controller.GetEntryCount());
EXPECT_FALSE(controller.GetLastCommittedEntry());
// Navigating the window to about:blank#foo will be classified as a same-
// document NEW_ENTRY, and will add a new entry.
NavigateWindowAndCheckNavigationTypeIsNewEntry(
new_contents, GURL("about:blank#foo"),
true /* wait_for_previous_navigations */,
true /* expect_same_document */);
EXPECT_EQ(1, controller.GetEntryCount());
EXPECT_TRUE(controller.GetLastCommittedEntry());
}
// 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, which will create a
// NavigationEntry.
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();
EXPECT_TRUE(last_entry);
// Do a navigation on the window to about:blank#foo, creating a
// same-document navigation.
NavigateWindowAndCheckNavigationTypeIsNewEntry(
new_contents, GURL("about:blank#foo"),
true /* wait_for_previous_navigations */,
true /* expect_same_document */);
EXPECT_EQ(2, controller.GetEntryCount());
EXPECT_NE(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();
// Navigating the window to |url_2| will be classified as NEW_ENTRY and will
// add a new entry.
NavigateWindowAndCheckNavigationTypeIsNewEntry(new_contents, url_2);
EXPECT_EQ(3, controller.GetEntryCount());
EXPECT_NE(last_entry, controller.GetLastCommittedEntry());
}
// 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
// not create a 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(0, controller.GetEntryCount());
EXPECT_FALSE(controller.GetLastCommittedEntry());
// Navigate to |url_2|, and ensure that we won't wait for the |hung_url|
// navigation to finish.
NavigateWindowAndCheckNavigationTypeIsNewEntry(
new_contents, url_2, false /* wait_for_previous_navigations */);
EXPECT_EQ(1, controller.GetEntryCount());
EXPECT_TRUE(controller.GetLastCommittedEntry());
}
// 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 that won't create a NavigationEntry.
Shell* new_shell = OpenBlankWindow(contents());
WebContentsImpl* new_contents =
static_cast<WebContentsImpl*>(new_shell->web_contents());
NavigationControllerImpl& controller = new_contents->GetController();
EXPECT_EQ(0, controller.GetEntryCount());
EXPECT_FALSE(controller.GetLastCommittedEntry());
// The window should be on its initial empty document.
FrameTreeNode* new_root = new_contents->GetFrameTree()->root();
EXPECT_FALSE(new_root->has_committed_real_load());
EXPECT_TRUE(
new_root->is_on_initial_empty_document_or_subsequent_empty_documents());
// 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(0, controller.GetEntryCount());
EXPECT_EQ(GURL("about:blank"),
new_contents->GetMainFrame()->GetLastCommittedURL());
EXPECT_EQ(main_window_url,
new_contents->GetMainFrame()->last_url_in_renderer());
// The window lost its "initial empty document" status, but
// `has_committed_real_load` is still false because the last committed URL
// stays the same.
EXPECT_FALSE(new_root->has_committed_real_load());
EXPECT_FALSE(
new_root->is_on_initial_empty_document_or_subsequent_empty_documents());
// Navigating the window to |url_2| will be classified as NEW_ENTRY and will
// add a new entry.
NavigateWindowAndCheckNavigationTypeIsNewEntry(new_contents, url_2);
EXPECT_EQ(1, controller.GetEntryCount());
EXPECT_TRUE(controller.GetLastCommittedEntry());
}
// 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 not create a 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(0, controller.GetEntryCount());
EXPECT_FALSE(controller.GetLastCommittedEntry());
// Navigating the window to |url_2| will be classified as NEW_ENTRY and will
// add a new entry.
NavigateWindowAndCheckNavigationTypeIsNewEntry(new_contents, url_2);
EXPECT_EQ(1, controller.GetEntryCount());
EXPECT_TRUE(controller.GetLastCommittedEntry());
}
}
// 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 won't create a 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->GetFrameTree()->root();
EXPECT_EQ(0, controller.GetEntryCount());
EXPECT_FALSE(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 reloading a synchronously loaded about:blank document (whose load
// is triggered by browsing context creation) won't crash.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
ReloadSynchronouslyLoadedAboutBlankDocument) {
GURL main_window_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_window_url));
// Create a new blank window that won't create a NavigationEntry. This will
// trigger a synchronous load to about:blank.
Shell* new_shell = OpenBlankWindow(contents());
WebContentsImpl* new_contents =
static_cast<WebContentsImpl*>(new_shell->web_contents());
NavigationControllerImpl& controller = new_contents->GetController();
FrameTreeNode* root = new_contents->GetFrameTree()->root();
EXPECT_EQ(0, controller.GetEntryCount());
EXPECT_FALSE(controller.GetLastCommittedEntry());
{
// Reload the tab. The navigation will be ignored by the browser because
// there is no NavigationEntry for it.
// TODO(https://crbug.com/524208): Fix this.
NoNavigationsObserver observer(new_contents);
controller.Reload(ReloadType::NORMAL, false /* check_for_repost */);
// Check that the renderer is still alive and no NavigationEntry is added.
EXPECT_TRUE(ExecJs(new_shell, "true"));
EXPECT_EQ(0, controller.GetEntryCount());
}
{
// Reload the tab, bypassing the cache. The navigation will be ignored by
// the browser because there is no NavigationEntry for it.
// TODO(https://crbug.com/524208): Fix this.
NoNavigationsObserver observer(new_contents);
controller.Reload(ReloadType::BYPASSING_CACHE,
false /* check_for_repost */);
// Check that the renderer is still alive and no NavigationEntry is added.
EXPECT_TRUE(ExecJs(new_shell, "true"));
EXPECT_EQ(0, controller.GetEntryCount());
}
// Navigate the tab to a page that has an about:blank iframe, which will load
// the about:blank page synchronously.
GURL url_with_synchronous_blank_iframe(embedded_test_server()->GetURL(
"/navigation_controller/page_with_iframe.html"));
EXPECT_TRUE(NavigateToURL(new_shell, url_with_synchronous_blank_iframe));
EXPECT_EQ(1, controller.GetEntryCount());
{
// Reload the tab. This also reloads the iframe and re-triggers the
// synchronous about:blank navigation.
FrameNavigateParamsCapturer capturer(root);
controller.Reload(ReloadType::NORMAL, false /* check_for_repost */);
capturer.Wait();
EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
capturer.transition(), ui::PAGE_TRANSITION_RELOAD));
EXPECT_EQ(NAVIGATION_TYPE_EXISTING_ENTRY, capturer.navigation_type());
// Check that the renderer is still alive and no new navigation entry is
// added.
EXPECT_TRUE(ExecJs(new_shell, "true"));
EXPECT_TRUE(ExecJs(root->child_at(0), "true"));
EXPECT_EQ(1, controller.GetEntryCount());
}
{
// Reload the tab, bypassing the cache. This also reloads the iframe and
// re-triggers the synchronous about:blank navigation.
FrameNavigateParamsCapturer capturer(root);
controller.Reload(ReloadType::BYPASSING_CACHE,
false /* check_for_repost */);
capturer.Wait();
EXPECT_TRUE(ui::PageTransitionTypeIncludingQualifiersIs(
capturer.transition(), ui::PAGE_TRANSITION_RELOAD));
EXPECT_EQ(NAVIGATION_TYPE_EXISTING_ENTRY, capturer.navigation_type());
// Check that the renderer is still alive and no new navigation entry is
// added.
EXPECT_TRUE(ExecJs(new_shell, "true"));
EXPECT_TRUE(ExecJs(root->child_at(0), "true"));
EXPECT_EQ(1, controller.GetEntryCount());
}
}
// 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())
->GetFrameTree()
->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_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_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.
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 a client-side redirect, but doesn't actually result
// in entry 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(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 a client-side redirect, but doesn't actually result
// in entry 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());
}
{
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())
->GetFrameTree()
->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_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_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())
->GetFrameTree()
->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, which should not count as real committed loads.
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())
->GetFrameTree()
->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, but this does not count as committing a real load.
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_FALSE(root->child_at(0)->has_committed_real_load());
// 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, but this does not count as committing a real load.
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_FALSE(root->child_at(0)->child_at(0)->has_committed_real_load());
// 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, but this does not count as committing a real load.
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_FALSE(root->child_at(1)->has_committed_real_load());
// 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_FALSE(root->child_at(0)->has_committed_real_load());
EXPECT_TRUE(root->child_at(0)->child_at(0)->has_committed_real_load());
EXPECT_FALSE(root->child_at(1)->has_committed_real_load());
// 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_FALSE(root->child_at(0)->has_committed_real_load());
EXPECT_TRUE(root->child_at(0)->child_at(0)->has_committed_real_load());
EXPECT_TRUE(root->child_at(1)->has_committed_real_load());
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_FALSE(root->child_at(0)->has_committed_real_load());
EXPECT_TRUE(root->child_at(0)->child_at(0)->has_committed_real_load());
EXPECT_TRUE(root->child_at(1)->has_committed_real_load());
// 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())
->GetFrameTree()
->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())
->GetFrameTree()
->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_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())
->GetFrameTree()
->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_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())
->GetFrameTree()
->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())
->GetFrameTree()
->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_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 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())
->GetFrameTree()
->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_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())
->GetFrameTree()
->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())
->GetFrameTree()
->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())
->GetFrameTree()
->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_TRUE(root->child_at(0)->has_committed_real_load());
// 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_TRUE(root->child_at(1)->has_committed_real_load());
// 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())
->GetFrameTree()
->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())
->GetFrameTree()
->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())
->GetFrameTree()
->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())
->GetFrameTree()
->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())
->GetFrameTree()
->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(), content::BackForwardCache::TEST_ASSUMES_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())
->GetFrameTree()
->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(), content::BackForwardCache::TEST_ASSUMES_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())
->GetFrameTree()
->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) {
// 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())
->GetFrameTree()
->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) {
// 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())
->GetFrameTree()
->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())
->GetFrameTree()
->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())
->GetFrameTree()
->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) {
// 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())
->GetFrameTree()
->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) {
// 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())
->GetFrameTree()
->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())
->GetFrameTree()
->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(), 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());
restored_entry->SetPageState(entry2->GetPageState());
// 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())
->GetFrameTree()
->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())
->GetFrameTree()
->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, ui::PAGE_TRANSITION_RELOAD,
false, std::string(), controller.GetBrowserContext(),
nullptr /* blob_url_loader_factory */));
restored_entry->SetPageState(blink::PageState::CreateFromURL(main_url));
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"));
}
// 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())
->GetFrameTree()
->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);
navigation_manager_1.WaitForNavigationFinished(); // Initial navigation.
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())
->GetFrameTree()
->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())
->GetFrameTree()
->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())
->GetFrameTree()
->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())
->GetFrameTree()
->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) {
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->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())
->GetFrameTree()
->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()->GetMainFrame()->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())
->GetFrameTree()
->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_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())
->GetFrameTree()
->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 = static_cast<NavigationControllerImpl&>(
shell()->web_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(shell()->web_contents(),
PAGE_TYPE_NORMAL));
EXPECT_TRUE(shell()->web_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());
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);
// 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);
}
// 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* entry = controller.GetLastCommittedEntry();
EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
EXPECT_EQ(entry->GetRedirectChain()[0], main_url);
// Check that the FrameNavigationEntry's redirect chain contains 2 urls.
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->redirect_chain().size(), 2u);
EXPECT_EQ(frame_entry->redirect_chain()[0], iframe_redirect_url);
EXPECT_EQ(frame_entry->redirect_chain()[1], iframe_final_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 = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->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 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* entry = controller.GetLastCommittedEntry();
EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
EXPECT_EQ(entry->GetRedirectChain()[0], main_url);
// Check that the FrameNavigationEntry's redirect chain contains 2 urls.
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->redirect_chain().size(), 2u);
EXPECT_EQ(frame_entry->redirect_chain()[0], frame_redirect_url);
EXPECT_EQ(frame_entry->redirect_chain()[1], frame_final_url);
}
// Checks the contents of the redirect chain after same-document navigations.
IN_PROC_BROWSER_TEST_P(
NavigationControllerBrowserTest,
FrameNavigationEntry_MainFrameRedirectChain_NormalThenSameDocNavigations) {
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_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);
}
GURL fragment_url(embedded_test_server()->GetURL("/title1.html#foo"));
{
// Renderer-initiated fragment navigation.
TestNavigationManager navigation_manager(shell()->web_contents(),
fragment_url);
EXPECT_TRUE(ExecJs(contents(), "location.href = '#foo'"));
navigation_manager.WaitForNavigationFinished();
EXPECT_EQ(2, 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 always classified as client redirects. 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);
// No replaced entry because it's not a "real" 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 client redirect.
EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);
}
{
// History API same-document navigation through history.pushState.
GURL push_state_url(embedded_test_server()->GetURL("/title1.html#bar"));
TestNavigationManager navigation_manager(shell()->web_contents(),
push_state_url);
EXPECT_TRUE(
ExecJs(shell(), "history.pushState({}, '', '/title1.html#bar')"));
navigation_manager.WaitForNavigationFinished();
EXPECT_EQ(3, controller.GetEntryCount());
NavigationEntry* entry = controller.GetLastCommittedEntry();
EXPECT_EQ(push_state_url, entry->GetURL());
EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
// On History API navigations, the redirect chain contains the previous URL,
// then the fragment URL, because script-initiated navigations are always
// classified as client redirects. 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], push_state_url);
// No replaced entry because it's not a "real" 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 client redirect.
EXPECT_EQ(entry->GetOriginalRequestURL(), fragment_url);
}
{
// Browser-initiated fragment navigation.
GURL fragment_url_2(embedded_test_server()->GetURL("/title1.html#baz"));
EXPECT_TRUE(NavigateToURL(shell(), fragment_url_2));
EXPECT_EQ(4, 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);
// 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(), fragment_url_2);
}
}
// Checks the contents of the redirect chain after same-site navigations.
IN_PROC_BROWSER_TEST_P(
NavigationControllerBrowserTest,
FrameNavigationEntry_MainFrameRedirectChain_NormalThenSameSiteNavigations) {
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_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);
}
GURL url_2(embedded_test_server()->GetURL("/title2.html"));
{
// Renderer-initiated same-site navigation.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url_2));
EXPECT_EQ(2, controller.GetEntryCount());
NavigationEntry* entry = controller.GetLastCommittedEntry();
EXPECT_EQ(url_2, entry->GetURL());
// On script-initiated same-site navigations with no redirects, the
// redirect chain contains the previous page's URL, because script-initiated
// navigations are always classified as client redirects. 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);
// No replaced entry because it's not a "real" 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 client redirect.
EXPECT_EQ(entry->GetOriginalRequestURL(), entry->GetRedirectChain()[0]);
}
{
// Browser-initiated same-site navigation.
GURL url_3(embedded_test_server()->GetURL("/title3.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_3));
EXPECT_EQ(3, 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);
}
}
// Checks the contents of the redirect chain after cross-site navigations.
IN_PROC_BROWSER_TEST_P(
NavigationControllerBrowserTest,
FrameNavigationEntry_MainFrameRedirectChain_NormalThenCrossSiteNavigations) {
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_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);
}
GURL url_2(embedded_test_server()->GetURL("b.com", "/title2.html"));
{
// Renderer-initiated cross-site navigation.
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), url_2));
EXPECT_EQ(2, controller.GetEntryCount());
NavigationEntry* entry = controller.GetLastCommittedEntry();
EXPECT_EQ(url_2, entry->GetURL());
// On script-initiated cross-site navigations with no redirects, the
// redirect chain contains the previous page's URL, because script-initiated
// navigations are always classified as client redirects. 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);
// No replaced entry because it's not a "real" 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 client redirect.
EXPECT_EQ(entry->GetOriginalRequestURL(), start_url);
}
{
// Browser-initiated cross-site navigation.
GURL url_3(embedded_test_server()->GetURL("c.com", "/title3.html"));
EXPECT_TRUE(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, 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);
}
}
// Checks the contents of the redirect chain after reloads.
IN_PROC_BROWSER_TEST_P(
NavigationControllerBrowserTest,
FrameNavigationEntry_MainFrameRedirectChain_NormalThenReloads) {
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_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);
}
{
// Renderer-initiated reload.
TestNavigationManager navigation_manager(shell()->web_contents(),
start_url);
EXPECT_TRUE(ExecJs(contents(), "location.reload();"));
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);
}
{
// Browser-initiated tab reload.
TestNavigationManager navigation_manager(shell()->web_contents(),
start_url);
shell()->Reload();
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 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 also the final URL.
EXPECT_EQ(entry->GetOriginalRequestURL(), 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(shell()->web_contents(),
start_url);
shell()->web_contents()->GetMainFrame()->Reload();
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 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 also the final URL.
EXPECT_EQ(entry->GetOriginalRequestURL(), 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 = static_cast<NavigationControllerImpl&>(
shell()->web_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"));
{
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);
}
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
FrameTreeNode* iframe = root->child_at(0);
GURL iframe_url = iframe->current_url();
{
// Renderer-initiated reload on the iframe.
TestNavigationManager navigation_manager(shell()->web_contents(),
iframe_url);
EXPECT_TRUE(ExecJs(iframe, "location.reload();"));
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);
}
{
// Browser-initiated reload.
TestNavigationManager navigation_manager(shell()->web_contents(),
iframe_url);
root->child_at(0)->current_frame_host()->Reload();
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);
}
}
// Checks the contents of the redirect chain after navigation to an error page.
IN_PROC_BROWSER_TEST_P(
NavigationControllerBrowserTest,
FrameNavigationEntry_MainFrameRedirectChain_NormalThenErrorPage) {
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_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);
}
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);
}
{
// 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);
}
}
// 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 = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
GURL server_redirecting_url(
embedded_test_server()->GetURL("/server-redirect?/empty404.html"));
// Browser-initiated cross-site navigation that server-redirects to an empty
// 404 page, which would result in an error page.
GURL fail_url(embedded_test_server()->GetURL("/empty404.html"));
EXPECT_FALSE(NavigateToURL(shell(), fail_url));
EXPECT_EQ(1, controller.GetEntryCount());
NavigationEntry* entry = controller.GetLastCommittedEntry();
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_EQ(entry->GetRedirectChain().size(), 1u);
EXPECT_EQ(entry->GetRedirectChain()[0], 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
// be the URL of the page that failed to load even if the navigation went
// through server redirects.
EXPECT_EQ(entry->GetOriginalRequestURL(), fail_url);
}
// 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 = static_cast<NavigationControllerImpl&>(
shell()->web_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);
}
{
// Renderer-initiated cross-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 only
// contains the final URL, even if the navigation went through client
// and server redirects.
EXPECT_EQ(entry->GetRedirectChain().size(), 1u);
EXPECT_EQ(entry->GetRedirectChain()[0], 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
// be the URL of the page that failed to load even if the navigation went
// through client and server redirects.
EXPECT_EQ(entry->GetOriginalRequestURL(), fail_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 = static_cast<NavigationControllerImpl&>(
shell()->web_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);
}
// 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 = static_cast<NavigationControllerImpl&>(
shell()->web_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 final_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(shell()->web_contents(),
client_redirecting_url);
TestNavigationManager navigation_manager_2(shell()->web_contents(),
final_url);
shell()->LoadURL(client_redirecting_url);
navigation_manager_1.WaitForNavigationFinished(); // Initial navigation.
navigation_manager_2.WaitForNavigationFinished(); // Client-side redirect.
ASSERT_EQ(1, controller.GetEntryCount());
NavigationEntry* entry = controller.GetEntryAtIndex(0);
ASSERT_EQ(final_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], 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);
}
{
// Renderer-initiated fragment navigation.
GURL fragment_url(embedded_test_server()->GetURL(
"/navigation_controller/simple_page_1.html#foo"));
TestNavigationManager navigation_manager(shell()->web_contents(),
fragment_url);
EXPECT_TRUE(ExecJs(contents(), "location.href = '#foo'"));
navigation_manager.WaitForNavigationFinished();
EXPECT_EQ(2, controller.GetEntryCount());
NavigationEntry* entry = controller.GetLastCommittedEntry();
EXPECT_EQ(fragment_url, entry->GetURL());
EXPECT_EQ(entry->GetRedirectChain().size(), 2u);
// When a renderer-initiated fragment navigation 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], final_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.
EXPECT_EQ(entry->GetOriginalRequestURL(), final_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 = static_cast<NavigationControllerImpl&>(
shell()->web_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 final_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(shell()->web_contents(),
client_redirecting_url);
TestNavigationManager navigation_manager_2(shell()->web_contents(),
final_url);
shell()->LoadURL(client_redirecting_url);
navigation_manager_1.WaitForNavigationFinished(); // Initial navigation.
navigation_manager_2.WaitForNavigationFinished(); // Client-side redirect.
ASSERT_EQ(1, controller.GetEntryCount());
NavigationEntry* entry = controller.GetEntryAtIndex(0);
ASSERT_EQ(final_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], 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);
}
{
// Renderer-initiated fragment navigation.
GURL fragment_url(embedded_test_server()->GetURL(
"/navigation_controller/client_redirect_fragment.html#bar"));
TestNavigationManager navigation_manager(shell()->web_contents(),
fragment_url);
EXPECT_TRUE(ExecJs(contents(), "location.href = '#bar'"));
navigation_manager.WaitForNavigationFinished();
EXPECT_EQ(2, controller.GetEntryCount());
NavigationEntry* entry = controller.GetLastCommittedEntry();
EXPECT_EQ(fragment_url, entry->GetURL());
// When a renderer-initiated fragment navigation 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], final_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.
EXPECT_EQ(entry->GetOriginalRequestURL(), final_url);
}
}
// 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 = static_cast<NavigationControllerImpl&>(
shell()->web_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(shell()->web_contents(),
server_redirecting_url);
TestNavigationManager navigation_manager_2(shell()->web_contents(),
final_url);
shell()->LoadURL(server_redirecting_url);
navigation_manager_1.WaitForNavigationFinished(); // Initial navigation.
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);
}
}
// 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 = static_cast<NavigationControllerImpl&>(
shell()->web_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(shell()->web_contents(),
client_redirecting_url);
TestNavigationManager navigation_manager_2(shell()->web_contents(),
server_redirecting_url);
shell()->LoadURL(client_redirecting_url);
navigation_manager_1.WaitForNavigationFinished(); // Initial navigation +
// client-side redirect.
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);
}
}
// 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())
->GetFrameTree()
->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, ui::PAGE_TRANSITION_RELOAD,
false, std::string(), controller.GetBrowserContext(),
nullptr /* blob_url_loader_factory */));
EXPECT_EQ(0U, restored_entry->root_node()->children.size());
restored_entry->SetPageState(entry2->GetPageState());
// 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())
->GetFrameTree()
->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())
->GetFrameTree()
->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_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())
->GetFrameTree()
->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_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());
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->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())
->GetFrameTree()
->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())
->GetFrameTree()
->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())
->GetFrameTree()
->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());
// TODO(japhet): This case fails because we are not correctly matching and
// de-duplicating FrameNavigationEntries when cloning.
// https://crbug.com/1211683
//
// 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->GetFrameTree()->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());
}
namespace {
class FailureWatcher : public WebContentsObserver {
public:
// Observes failure for the specified |node|.
explicit FailureWatcher(FrameTreeNode* node)
: WebContentsObserver(
node->current_frame_host()->delegate()->GetAsWebContents()),
frame_tree_node_id_(node->frame_tree_node_id()),
message_loop_runner_(new MessageLoopRunner) {}
void Wait() { message_loop_runner_->Run(); }
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())
->GetFrameTree()
->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 RenderProcessGone(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())
->GetFrameTree()
->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_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));
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 is elided from the
// navigation history of a subframe when it is loaded.
//
// 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,
BackToAboutBlankIframe) {
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())
->GetFrameTree()
->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. Note that the old navigation entry has
// "about:blank" as the URL in the iframe.
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.
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();
}
EXPECT_EQ(2, controller.GetEntryCount());
EXPECT_EQ(2, EvalJs(shell(), "history.length"));
EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
// There is some open discussion over whether this should send the iframe
// back to the blank page, but for now it stays in place to preserve
// compatibility with existing sites. See
// NavigationControllerImpl::FindFramesToNavigate for more information, as
// well as http://crbug.com/542299, https://crbug.com/598043 (for the
// regressions caused by going back), and
// https://github.com/whatwg/html/issues/546.
// TODO(avi, creis): Figure out the correct behavior to use here.
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_2 = embedded_test_server()->GetURL(
"foo.com", "/navigation_controller/simple_page_2.html#foo");
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());
// TODO(creis): We should probably go back to frame_url here instead of the
// initial blank page. That might require updating all relevant NavEntries to
// know what the first committed URL is, so that we really elide the initial
// blank page from history.
EXPECT_EQ(blank_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())
->GetFrameTree()
->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());
// There is some open discussion over whether this should send the iframe back
// to the original page, but for now it stays in place to preserve
// compatibility with existing sites. See
// NavigationControllerImpl::FindFramesToNavigate for more information, as
// well as http://crbug.com/542299, https://crbug.com/598043 (for the
// regressions caused by going back), and
// https://github.com/whatwg/html/issues/546.
// TODO(avi, creis): Figure out the correct behavior to use here.
EXPECT_EQ(frame_url_2, 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())
->GetFrameTree()
->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());
// There is some open discussion over whether this should send the iframe back
// to the original page, but for now it stays in place to preserve
// compatibility with existing sites. See
// NavigationControllerImpl::FindFramesToNavigate for more information, as
// well as http://crbug.com/542299, https://crbug.com/598043 (for the
// regressions caused by going back), and
// https://github.com/whatwg/html/issues/546.
// TODO(avi, creis): Figure out the correct behavior to use here.
EXPECT_EQ(frame_url_3, 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())
->GetFrameTree()
->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())
->GetFrameTree()
->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) {
// 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())
->GetFrameTree()
->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_b1(url::kAboutBlankURL);
EXPECT_TRUE(NavigateToURL(shell(), url_b));
EXPECT_EQ(3, controller.GetEntryCount());
EXPECT_EQ(url_b, root->current_url());
EXPECT_EQ(frame_url_b1, 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.
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.
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_b1, 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();
TestNavigationManager back_manager(
shell()->web_contents(), embedded_test_server()->GetURL("/title1.html"));
controller.GoBack();
back_manager.WaitForNavigationFinished();
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.
// Currently flaking on Android and Mac, see https://crubug.com/1101292.
#if defined(OS_ANDROID) || defined(OS_MAC)
#define MAYBE_PageStateAfterForwardInCompetingFrames \
DISABLED_PageStateAfterForwardInCompetingFrames
#else
#define MAYBE_PageStateAfterForwardInCompetingFrames \
PageStateAfterForwardInCompetingFrames
#endif
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
MAYBE_PageStateAfterForwardInCompetingFrames) {
// 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())
->GetFrameTree()
->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.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
PageStateWithIframeAfterForwardInCompetingFrames) {
// TODO(https://crbug.com/1101292): Remove this when test passes. It only
// fails on ASAN builder.
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 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())
->GetFrameTree()
->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())
->GetFrameTree()
->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->GetFrameTree()->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())
->GetFrameTree()
->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())
->GetFrameTree()
->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->GetFrameTree()->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->GetFrameTree()->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->GetFrameTree()->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() 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_;
DISALLOW_COPY_AND_ASSIGN(HistoryNavigationBeforeCommitInjector);
};
// 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) {
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetFrameTree()->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));
// The test below only makes sense for same-site same-RFH navigations, so we
// need to ensure that we won't trigger a same-site cross-RFH navigation.
DisableProactiveBrowsingInstanceSwapFor(root->current_frame_host());
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) {
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
FrameTreeNode* root = web_contents->GetFrameTree()->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));
// The test below only makes sense for same-site same-RFH navigations, so we
// need to ensure that we won't trigger a same-site cross-RFH navigation.
DisableProactiveBrowsingInstanceSwapFor(root->current_frame_host());
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::TimeDelta::FromMicroseconds(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->GetFrameTree()->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());
// 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")));
content::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;
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:
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->GetMainFrame()));
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 ContentBrowserTest,
public ::testing::WithParamInterface<std::string> {
public:
RequestMonitoringNavigationBrowserTest() {
InitAndEnableRenderDocumentFeature(&feature_list_for_render_document_,
GetParam());
}
const net::test_server::HttpRequest* FindAccumulatedRequest(
const GURL& url_to_find) {
DCHECK(url_to_find.SchemeIsHTTPOrHTTPS());
auto it = std::find_if(
accumulated_requests_.begin(), accumulated_requests_.end(),
[&url_to_find](const net::test_server::HttpRequest& request) {
return request.GetURL() == url_to_find;
});
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::SequencedTaskRunnerHandle::Get()));
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_;
base::test::ScopedFeatureList feature_list_for_render_document_;
// 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())
->GetFrameTree()
->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.
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 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://cbug.com/941653.
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
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())
->GetFrameTree()
->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());
EXPECT_FALSE(
new_shell->web_contents()->GetController().GetLastCommittedEntry());
// 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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->last_http_status_code());
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->last_http_status_code());
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->last_http_status_code());
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->last_http_status_code());
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(404, contents()->GetMainFrame()->last_http_status_code());
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(404, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(404, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(404, contents()->GetMainFrame()->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) {
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())
->GetFrameTree()
->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) {
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())
->GetFrameTree()
->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())
->GetFrameTree()
->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())
->GetFrameTree()
->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_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())
->GetFrameTree()
->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;
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())
->GetFrameTree()
->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 = shell()->web_contents()->GetAllFrames()[1];
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 = shell()->web_contents()->GetAllFrames()[1];
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()->GetMainFrame()->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()->GetMainFrame()->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(), content::BackForwardCache::TEST_ASSUMES_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()->GetMainFrame()->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()->GetMainFrame()->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())
->GetFrameTree()
->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.
TestNavigationObserver 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_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.
TestNavigationObserver 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_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.
TestNavigationObserver 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_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->gesture = NavigationGestureUser;
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()->GetMainFrame();
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()->GetMainFrame()->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()->GetMainFrame()->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->GetMainFrame()->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()->GetMainFrame()->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()->GetMainFrame()->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, 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()->GetMainFrame()->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()->GetMainFrame()->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()->GetMainFrame()->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()->GetMainFrame()->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()->GetMainFrame()->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()->GetMainFrame()->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()->GetMainFrame()->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()->GetMainFrame()->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()->GetMainFrame()->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()->GetMainFrame()->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()->GetMainFrame()->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())
->GetFrameTree()
->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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())
->GetFrameTree()
->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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()->GetMainFrame()->last_http_method());
EXPECT_EQ(200, contents()->GetMainFrame()->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())
->GetFrameTree()
->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())
->GetFrameTree()
->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(), content::BackForwardCache::TEST_ASSUMES_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_NEW_ENTRY, capturer.navigation_type());
} else {
EXPECT_EQ(initial_site_instance,
root->current_frame_host()->GetSiteInstance());
EXPECT_EQ(NAVIGATION_TYPE_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())
->GetFrameTree()
->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());
}
{
// 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());
}
}
// 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())
->GetFrameTree()
->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());
}
{
// 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());
}
}
// 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())
->GetFrameTree()
->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());
}
{
// 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());
}
}
// 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())
->GetFrameTree()
->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());
}
{
// 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());
}
}
// 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->GetFrameTree()->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());
}
{
// 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());
}
{
// 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());
}
{
// 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());
}
{
// 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());
}
}
// 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())
->GetFrameTree()
->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());
}
{
// 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());
}
{
// 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());
}
{
// 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());
}
}
// history.back() called twice in the renderer process should not make the user
// navigate back twice.
// Regression test for https://crbug.com/869710
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
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()->GetMainFrame();
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()->GetMainFrame()->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.
auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
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(policy->GetProcessLock(error_site_instance->GetProcess()->GetID())
.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()->GetMainFrame(), 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()->GetMainFrame(), 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.
IN_PROC_BROWSER_TEST_P(
NavigationControllerBrowserTest,
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()->GetMainFrame();
// 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(), content::BackForwardCache::TEST_ASSUMES_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()->GetMainFrame(), 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()->GetMainFrame(), 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->GetMainFrame();
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->GetMainFrame();
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()->GetMainFrame(),
"!!(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()->GetMainFrame(), 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()->GetMainFrame(),
"!!(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()->GetMainFrame(), 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()->GetMainFrame(),
"!!(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()->GetMainFrame(), 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()->GetMainFrame(),
"!!(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()->GetMainFrame(),
"!!(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;
}
Shell* shell_;
const GURL& url_to_intercept_;
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()->GetFrameTree()->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());
}
using NavigationControllerHistoryInterventionBrowserTest =
NavigationControllerBrowserTest;
class NavigationControllerDisableHistoryIntervention
: public NavigationControllerBrowserTest {
protected:
void SetUp() override {
feature_list_.InitAndDisableFeature(
features::kHistoryManipulationIntervention);
NavigationControllerBrowserTest::SetUp();
}
private:
base::test::ScopedFeatureList feature_list_;
};
#if defined(OS_ANDROID)
// Test GoToOffset with the intervention disabled.
IN_PROC_BROWSER_TEST_P(NavigationControllerDisableHistoryIntervention,
GoToOffsetWithSkippingDisableHistoryIntervention) {
base::HistogramTester histograms;
GURL non_skippable_url(
embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_url));
GURL skippable_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
GURL skippable_url2(embedded_test_server()->GetURL("/title3.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_url2));
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url2(embedded_test_server()->GetURL("/title4.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url2));
// The results of CanGoToOffset and CanGoToOffsetWithSkipping should be the
// same when the intervention is disabled.
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_TRUE(controller.CanGoToOffset(-3));
EXPECT_TRUE(controller.CanGoToOffset(-4));
EXPECT_TRUE(controller.CanGoToOffsetWithSkipping(-3));
EXPECT_TRUE(controller.CanGoToOffsetWithSkipping(-4));
// Ignores the history manipulation intervention and visits skippable entries.
TestNavigationObserver nav_observer(shell()->web_contents());
controller.GoToOffsetWithSkipping(-4);
nav_observer.Wait();
EXPECT_EQ(0, controller.GetCurrentEntryIndex());
EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
EXPECT_EQ(non_skippable_url, controller.GetLastCommittedEntry()->GetURL());
}
#endif // OS_ANDROID
// 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()->GetMainFrame(),
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()->GetMainFrame(),
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()->GetMainFrame(),
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 the navigation entry is marked as skippable on back/forward button
// if it does a renderer initiated navigation without ever getting a user
// activation.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
NoUserActivationSetSkipOnBackForward) {
base::HistogramTester histograms;
GURL non_skippable_url(
embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_url));
GURL skippable_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(2, controller.GetCurrentEntryIndex());
EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
// Last entry should have been marked as skippable.
EXPECT_TRUE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(
"Navigation.BackForward.SetShouldSkipOnBackForwardUI", true, 1);
EXPECT_TRUE(controller.CanGoBack());
// Attempt to go back or forward to the skippable entry should log the
// corresponding histogram and skip the corresponding entry.
TestNavigationObserver back_load_observer(shell()->web_contents());
controller.GoBack();
back_load_observer.Wait();
histograms.ExpectBucketCount("Navigation.BackForward.BackTargetSkipped", 1,
1);
histograms.ExpectBucketCount("Navigation.BackForward.AllBackTargetsSkippable",
false, 1);
EXPECT_EQ(non_skippable_url, controller.GetLastCommittedEntry()->GetURL());
EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
}
// Same as the above test except the navigation is cross-site in this case.
// Tests that the navigation entry is marked as skippable on back/forward button
// if it does a renderer initiated cross-site navigation without ever getting a
// user activation.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
NoUserActivationSetSkipOnBackForwardCrossSite) {
base::HistogramTester histograms;
GURL non_skippable_url(
embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_url));
GURL skippable_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new cross-site document from the renderer with a user
// gesture.
GURL redirected_url(
embedded_test_server()->GetURL("foo.com", "/title1.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(2, controller.GetCurrentEntryIndex());
EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
// Last entry should have been marked as skippable.
EXPECT_TRUE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(
"Navigation.BackForward.SetShouldSkipOnBackForwardUI", true, 1);
EXPECT_TRUE(controller.CanGoBack());
// Attempt to go back or forward to the skippable entry should log the
// corresponding histogram and skip the corresponding entry.
TestNavigationObserver back_load_observer(shell()->web_contents());
controller.GoBack();
back_load_observer.Wait();
histograms.ExpectBucketCount("Navigation.BackForward.BackTargetSkipped", 1,
1);
histograms.ExpectBucketCount("Navigation.BackForward.AllBackTargetsSkippable",
false, 1);
EXPECT_EQ(non_skippable_url, controller.GetLastCommittedEntry()->GetURL());
EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
}
// Tests that the navigation entry is marked as skippable on back/forward button
// but is not skipped if the feature is not enabled.
IN_PROC_BROWSER_TEST_P(NavigationControllerDisableHistoryIntervention,
NoSkipOnBackFeatureDisabled) {
base::HistogramTester histograms;
GURL non_skippable_url(
embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_url));
GURL skippable_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(2, controller.GetCurrentEntryIndex());
EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
// Last entry should have been marked as skippable.
EXPECT_TRUE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(
"Navigation.BackForward.SetShouldSkipOnBackForwardUI", true, 1);
EXPECT_TRUE(controller.CanGoBack());
// Attempt to go back or forward to the skippable entry should log the
// corresponding histogram.
TestNavigationObserver back_load_observer(shell()->web_contents());
controller.GoBack();
back_load_observer.Wait();
histograms.ExpectBucketCount("Navigation.BackForward.BackTargetSkipped", 1,
1);
// Since the feature is disabled, it will be navigated to the skippable entry.
EXPECT_EQ(skippable_url, controller.GetLastCommittedEntry()->GetURL());
EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
}
// Tests that the navigation entry is marked as skippable on back button if it
// does a renderer initiated navigation without ever getting a user activation.
// Also tests this for an entry added using history.pushState.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
NoUserActivationSetSkippableMultipleGoBack) {
base::HistogramTester histograms;
const std::string histogram_name =
"Navigation.BackForward.SetShouldSkipOnBackForwardUI";
GURL skippable_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
// Use the pushState API to add another entry without user gesture.
GURL push_state_url(embedded_test_server()->GetURL("/title2.html"));
std::string script("history.pushState('', '','" + push_state_url.spec() +
"');");
EXPECT_TRUE(
ExecJs(shell()->web_contents(), script, EXECUTE_SCRIPT_NO_USER_GESTURE));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(2, controller.GetCurrentEntryIndex());
EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
// Last 2 entries should have been marked as skippable.
EXPECT_TRUE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
EXPECT_TRUE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(histogram_name, true, 2);
// CanGoBack should return false since all previous entries are skippable.
EXPECT_FALSE(controller.CanGoBack());
// Attempt to go back to the entries marked to be skipped should log a
// histogram.
controller.GoBack(); // Will not go back
EXPECT_EQ(controller.GetLastCommittedEntryIndex(), 2);
histograms.ExpectBucketCount("Navigation.BackForward.BackTargetSkipped", 2,
1);
histograms.ExpectBucketCount("Navigation.BackForward.AllBackTargetsSkippable",
true, 1);
}
// Same as above but tests the metrics on going forward.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
NoUserActivationSetSkippableMultipleGoForward) {
base::HistogramTester histograms;
const std::string histogram_name =
"Navigation.BackForward.SetShouldSkipOnBackForwardUI";
GURL skippable_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
// Use the pushState API to add another entry without user gesture.
GURL push_state_url(embedded_test_server()->GetURL("/title2.html"));
std::string script("history.pushState('', '','" + push_state_url.spec() +
"');");
EXPECT_TRUE(
ExecJs(shell()->web_contents(), script, EXECUTE_SCRIPT_NO_USER_GESTURE));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(2, controller.GetCurrentEntryIndex());
EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
// Last 2 entries should have been marked as skippable.
EXPECT_TRUE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
EXPECT_TRUE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(histogram_name, true, 2);
// Go to the 1st entry.
TestNavigationObserver load_observer(shell()->web_contents());
controller.GoToIndex(0);
load_observer.Wait();
// Attempt to go forward to the entries marked to be skipped should log a
// histogram.
EXPECT_TRUE(controller.CanGoForward());
TestNavigationObserver back_load_observer(shell()->web_contents());
controller.GoForward();
back_load_observer.Wait();
histograms.ExpectBucketCount("Navigation.BackForward.ForwardTargetSkipped", 1,
1);
}
// Tests that if an entry is marked as skippable, it will not be reset if there
// is a navigation to this entry again (crbug.com/112129). This does not need
// the feature to be enabled.
IN_PROC_BROWSER_TEST_P(NavigationControllerDisableHistoryIntervention,
DoNotResetSkipOnBackForward) {
base::HistogramTester histograms;
GURL main_url(embedded_test_server()->GetURL("/frame_tree/top.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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURLFromRendererWithoutUserGesture(shell(), url));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(1, controller.GetCurrentEntryIndex());
EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
// Last entry should have been marked as skippable.
EXPECT_TRUE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(
"Navigation.BackForward.SetShouldSkipOnBackForwardUI", true, 1);
// Go back to the last entry.
TestNavigationObserver back_nav_load_observer(shell()->web_contents());
controller.GoToIndex(0);
back_nav_load_observer.Wait();
// Going back again to an entry should not reset its skippable flag.
EXPECT_TRUE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
// Navigating away from this with a browser initiated navigation should log a
// histogram with skippable as true.
GURL url1(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), url1));
histograms.ExpectBucketCount(
"Navigation.BackForward.SetShouldSkipOnBackForwardUI", true, 2);
}
// Tests that if an entry is marked as skippable, it will not be reset if there
// is a navigation to this entry again (crbug.com/112129) using history.back/
// forward. This does not need the feature to be enabled.
IN_PROC_BROWSER_TEST_P(NavigationControllerDisableHistoryIntervention,
DoNotResetSkipOnHistoryBackAPI) {
base::HistogramTester histograms;
GURL main_url(embedded_test_server()->GetURL("/frame_tree/top.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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURLFromRendererWithoutUserGesture(shell(), url));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(1, controller.GetCurrentEntryIndex());
EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
// Last entry should have been marked as skippable.
EXPECT_TRUE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(
"Navigation.BackForward.SetShouldSkipOnBackForwardUI", true, 1);
// Go back to the last entry using history.back.
EXPECT_TRUE(
ExecJs(shell(), "history.back();", EXECUTE_SCRIPT_NO_USER_GESTURE));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// Going back again to an entry should not reset its skippable flag.
EXPECT_TRUE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
}
// Tests that if a navigation entry is marked as skippable due to pushState then
// the flag should be reset if there is a user gesture on this document. All of
// the adjacent entries belonging to the same document will have their skippable
// bits reset.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
OnUserGestureResetSameDocumentEntriesSkipFlag) {
GURL skippable_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
// Redirect to another page without a user gesture.
GURL redirected_url(embedded_test_server()->GetURL("/empty.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
// Last entry should have been marked as skippable.
EXPECT_TRUE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
// Use the pushState API to add another entry without user gesture.
GURL push_state_url1(embedded_test_server()->GetURL("/title1.html"));
std::string script("history.pushState('', '','" + push_state_url1.spec() +
"');");
EXPECT_TRUE(
ExecJs(shell()->web_contents(), script, EXECUTE_SCRIPT_NO_USER_GESTURE));
// Use the pushState API to add another entry without user gesture.
GURL push_state_url2(embedded_test_server()->GetURL("/title2.html"));
script = "history.pushState('', '','" + push_state_url2.spec() + "');";
EXPECT_TRUE(
ExecJs(shell()->web_contents(), script, EXECUTE_SCRIPT_NO_USER_GESTURE));
EXPECT_EQ(3, controller.GetCurrentEntryIndex());
EXPECT_EQ(3, controller.GetLastCommittedEntryIndex());
// We now have
// [skippable_url(skip), redirected_url(skip), push_state_url1(skip),
// push_state_url2*]
// Last 2 entries should have been marked as skippable.
EXPECT_TRUE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_TRUE(controller.GetEntryAtIndex(2)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
EXPECT_EQ(skippable_url, controller.GetEntryAtIndex(0)->GetURL());
EXPECT_EQ(redirected_url, controller.GetEntryAtIndex(1)->GetURL());
EXPECT_EQ(push_state_url1, controller.GetEntryAtIndex(2)->GetURL());
EXPECT_EQ(push_state_url2, controller.GetEntryAtIndex(3)->GetURL());
// Do another pushState so push_state_url2's entry also becomes skippable.
GURL push_state_url3(embedded_test_server()->GetURL("/title3.html"));
script = "history.pushState('', '','" + push_state_url3.spec() + "');";
EXPECT_TRUE(
ExecJs(shell()->web_contents(), script, EXECUTE_SCRIPT_NO_USER_GESTURE));
EXPECT_TRUE(controller.GetEntryAtIndex(3)->should_skip_on_back_forward_ui());
// We now have
// [skippable_url(skip), redirected_url(skip), push_state_url1(skip),
// push_state_url2(skip), push_state_url3*]
// Go to index 2.
TestNavigationObserver load_observer(shell()->web_contents());
controller.GoToIndex(2);
load_observer.Wait();
EXPECT_EQ(push_state_url1, controller.GetLastCommittedEntry()->GetURL());
// We now have (Before user gesture)
// [skippable_url(skip), redirected_url(skip), push_state_url1(skip)*,
// push_state_url2(skip), push_state_url3]
// Note the entry at index 2 retains its skippable flag.
EXPECT_TRUE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
EXPECT_TRUE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_TRUE(controller.GetEntryAtIndex(2)->should_skip_on_back_forward_ui());
EXPECT_TRUE(controller.GetEntryAtIndex(3)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(4)->should_skip_on_back_forward_ui());
// Simulate a user gesture. ExecuteScript internally also sends a user
// gesture.
script = "a=5";
EXPECT_TRUE(content::ExecJs(shell()->web_contents(), script));
// We now have (After user gesture)
// [skippable_url(skip), redirected_url, push_state_url1*, push_state_url2,
// push_state_url3]
// All the navigations that refer to the same document should have their
// skippable bit reset.
EXPECT_FALSE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(2)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(3)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(4)->should_skip_on_back_forward_ui());
// The first entry is not the same document and its bit should not be reset.
EXPECT_TRUE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
// goBack should now navigate to entry at index 1.
TestNavigationObserver back_load_observer(shell()->web_contents());
controller.GoBack();
back_load_observer.Wait();
EXPECT_EQ(redirected_url, controller.GetLastCommittedEntry()->GetURL());
// Do another pushState without user gesture.
GURL push_state_url4(embedded_test_server()->GetURL("/title3.html"));
script = "history.pushState('', '','" + push_state_url3.spec() + "');";
EXPECT_TRUE(
ExecJs(shell()->web_contents(), script, EXECUTE_SCRIPT_NO_USER_GESTURE));
// We now have
// [skippable_url(skip), redirected_url, push_state_url4*]
EXPECT_EQ(3, controller.GetEntryCount());
EXPECT_EQ(skippable_url, controller.GetEntryAtIndex(0)->GetURL());
EXPECT_EQ(redirected_url, controller.GetEntryAtIndex(1)->GetURL());
EXPECT_EQ(push_state_url4, controller.GetEntryAtIndex(2)->GetURL());
// The skippable flag will still be unset since this page has seen a user
// gesture once.
EXPECT_FALSE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
}
// Tests that if a navigation entry is marked as skippable due to redirect to a
// new document then the flag should not be reset if there is a user gesture on
// the new document.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
OnUserGestureDoNotResetDifferentDocumentEntrySkipFlag) {
GURL skippable_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(1, controller.GetCurrentEntryIndex());
EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
EXPECT_TRUE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
// Simulate a user gesture.
root->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kNotifyActivation,
blink::mojom::UserActivationNotificationType::kTest);
// Since the last navigations refer to a different document, a user gesture
// here should not reset the skippable bit in the previous entries.
EXPECT_TRUE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
}
// Tests that the navigation entry is not marked as skippable on back/forward
// button if it does a renderer initiated navigation after getting a user
// activation.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
UserActivationDoNotSkipOnBackForward) {
base::HistogramTester histograms;
const std::string histogram_name =
"Navigation.BackForward.SetShouldSkipOnBackForwardUI";
GURL non_skippable_url(
embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer.
// Note that NavigateToURLFromRenderer also simulates a user gesture.
GURL user_gesture_redirected_url(
embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), user_gesture_redirected_url));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(1, controller.GetCurrentEntryIndex());
EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
// Last entry should not have been marked as skippable.
EXPECT_FALSE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(histogram_name, false, 1);
// Nothing should get skipped when back button is clicked.
TestNavigationObserver back_nav_load_observer(shell()->web_contents());
controller.GoBack();
back_nav_load_observer.Wait();
EXPECT_EQ(non_skippable_url, controller.GetLastCommittedEntry()->GetURL());
EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
histograms.ExpectBucketCount("Navigation.BackForward.BackTargetSkipped", 0,
1);
}
// Tests that the navigation entry should not be marked as skippable on
// back/forward button if it is navigated away using a browser initiated
// navigation.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
BrowserInitiatedNavigationDoNotSkipOnBackForward) {
base::HistogramTester histograms;
const std::string histogram_name =
"Navigation.BackForward.SetShouldSkipOnBackForwardUI";
GURL non_skippable_url(
embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
GURL url(embedded_test_server()->GetURL("/title1.html"));
// Note that NavigateToURL simulates a browser initiated navigation.
EXPECT_TRUE(NavigateToURL(shell(), url));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(1, controller.GetCurrentEntryIndex());
EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
// Last entry should not have been marked as skippable.
EXPECT_FALSE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(histogram_name, false, 1);
// Nothing should get skipped when back button is clicked.
TestNavigationObserver back_nav_load_observer(shell()->web_contents());
controller.GoBack();
back_nav_load_observer.Wait();
EXPECT_EQ(non_skippable_url, controller.GetLastCommittedEntry()->GetURL());
EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
}
// Tests that the navigation entry that is marked as skippable on back/forward
// button does not get skipped for history.back API calls.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
SetSkipOnBackDoNotSkipForHistoryBackAPI) {
base::HistogramTester histograms;
GURL non_skippable_url(
embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_url));
GURL skippable_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(2, controller.GetCurrentEntryIndex());
EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
// Last entry should have been marked as skippable.
EXPECT_TRUE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(
"Navigation.BackForward.SetShouldSkipOnBackForwardUI", true, 1);
// Attempt to go back to the skippable entry using the History API should
// not skip the corresponding entry.
TestNavigationObserver frame_observer(shell()->web_contents());
EXPECT_TRUE(ExecJs(root, "window.history.back()"));
frame_observer.Wait();
histograms.ExpectTotalCount("Navigation.BackForward.BackTargetSkipped", 0);
EXPECT_EQ(skippable_url, controller.GetLastCommittedEntry()->GetURL());
EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
}
#if defined(OS_ANDROID)
// Test GoToOffset with enable history intervention.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
GoToOffsetWithSkippingEnableHistoryIntervention) {
base::HistogramTester histograms;
GURL non_skippable_url(
embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_url));
GURL skippable_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
GURL skippable_url2(embedded_test_server()->GetURL("/title3.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_url2));
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url2(embedded_test_server()->GetURL("/title4.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url2));
// CanGoToOffset should visit the skippable entries while
// CanGoToOffsetWithSKipping will skip the skippable entries.
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_TRUE(controller.CanGoToOffset(-3));
EXPECT_TRUE(controller.CanGoToOffset(-4));
EXPECT_FALSE(controller.CanGoToOffsetWithSkipping(-3));
TestNavigationObserver nav_observer(shell()->web_contents());
controller.GoToOffset(-4);
nav_observer.Wait();
EXPECT_EQ(0, controller.GetCurrentEntryIndex());
EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
EXPECT_EQ(non_skippable_url, controller.GetLastCommittedEntry()->GetURL());
}
#endif // OS_ANDROID
// Tests that the navigation entry that is marked as skippable on back/forward
// button does not get skipped for GoToOffset calls.
// This covers actions in the following scenario:
// [non_skippable_url, skippable_url, redirected_url, skippable_url2,
// redirected_url2]
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
SetSkipOnBackForwardDoNotSkipForGoToOffset) {
base::HistogramTester histograms;
GURL non_skippable_url(
embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_url));
GURL skippable_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
GURL skippable_url2(embedded_test_server()->GetURL("/title3.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_url2));
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url2(embedded_test_server()->GetURL("/title4.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url2));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(4, controller.GetCurrentEntryIndex());
EXPECT_EQ(4, controller.GetLastCommittedEntryIndex());
EXPECT_TRUE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(2)->should_skip_on_back_forward_ui());
EXPECT_TRUE(controller.GetEntryAtIndex(3)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(4)->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(
"Navigation.BackForward.SetShouldSkipOnBackForwardUI", true, 2);
EXPECT_TRUE(controller.CanGoToOffset(-3));
// GoToOffset should visit the skippable entries.
TestNavigationObserver nav_observer1(shell()->web_contents());
controller.GoToOffset(-1);
nav_observer1.Wait();
EXPECT_EQ(3, controller.GetCurrentEntryIndex());
EXPECT_EQ(3, controller.GetLastCommittedEntryIndex());
EXPECT_EQ(skippable_url2, controller.GetLastCommittedEntry()->GetURL());
TestNavigationObserver nav_observer2(shell()->web_contents());
controller.GoToOffset(1);
nav_observer2.Wait();
EXPECT_EQ(4, controller.GetCurrentEntryIndex());
EXPECT_EQ(4, controller.GetLastCommittedEntryIndex());
EXPECT_EQ(redirected_url2, controller.GetLastCommittedEntry()->GetURL());
TestNavigationObserver nav_observer3(shell()->web_contents());
controller.GoToOffset(-4);
nav_observer3.Wait();
EXPECT_EQ(0, controller.GetCurrentEntryIndex());
EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
EXPECT_EQ(non_skippable_url, controller.GetLastCommittedEntry()->GetURL());
EXPECT_TRUE(controller.CanGoToOffset(4));
TestNavigationObserver nav_observer4(shell()->web_contents());
controller.GoToOffset(4);
nav_observer4.Wait();
EXPECT_EQ(4, controller.GetCurrentEntryIndex());
EXPECT_EQ(4, controller.GetLastCommittedEntryIndex());
EXPECT_EQ(redirected_url2, controller.GetLastCommittedEntry()->GetURL());
}
// Tests that the navigation entry that is marked as skippable on back/forward
// button is skipped for GoToOffset calls.
// This covers actions in the following scenario:
// [non_skippable_url, skippable_url, redirected_url, skippable_url2,
// redirected_url2]
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
SetSkipOnBackForwardDoSkipForGoToOffsetWithSkipping) {
#if defined(OS_ANDROID)
base::HistogramTester histograms;
GURL non_skippable_url(
embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_url));
GURL skippable_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
GURL skippable_url2(embedded_test_server()->GetURL("/title3.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_url2));
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url2(embedded_test_server()->GetURL("/title4.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url2));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(4, controller.GetCurrentEntryIndex());
EXPECT_EQ(4, controller.GetLastCommittedEntryIndex());
EXPECT_TRUE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(2)->should_skip_on_back_forward_ui());
EXPECT_TRUE(controller.GetEntryAtIndex(3)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(4)->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(
"Navigation.BackForward.SetShouldSkipOnBackForwardUI", true, 2);
EXPECT_FALSE(controller.CanGoToOffsetWithSkipping(-3));
EXPECT_TRUE(controller.CanGoToOffsetWithSkipping(-2));
// GoToOffset should skip the skippable entries.
TestNavigationObserver nav_observer1(shell()->web_contents());
controller.GoToOffsetWithSkipping(-1);
nav_observer1.Wait();
EXPECT_EQ(2, controller.GetCurrentEntryIndex());
EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
EXPECT_EQ(redirected_url, controller.GetLastCommittedEntry()->GetURL());
TestNavigationObserver nav_observer2(shell()->web_contents());
controller.GoToOffsetWithSkipping(1);
nav_observer2.Wait();
EXPECT_EQ(4, controller.GetCurrentEntryIndex());
EXPECT_EQ(4, controller.GetLastCommittedEntryIndex());
EXPECT_EQ(redirected_url2, controller.GetLastCommittedEntry()->GetURL());
TestNavigationObserver nav_observer3(shell()->web_contents());
controller.GoToOffsetWithSkipping(-2);
nav_observer3.Wait();
EXPECT_EQ(0, controller.GetCurrentEntryIndex());
EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
EXPECT_EQ(non_skippable_url, controller.GetLastCommittedEntry()->GetURL());
EXPECT_FALSE(controller.CanGoToOffsetWithSkipping(3));
EXPECT_TRUE(controller.CanGoToOffsetWithSkipping(2));
TestNavigationObserver nav_observer4(shell()->web_contents());
controller.GoToOffsetWithSkipping(2);
nav_observer4.Wait();
EXPECT_EQ(4, controller.GetCurrentEntryIndex());
EXPECT_EQ(4, controller.GetLastCommittedEntryIndex());
EXPECT_EQ(redirected_url2, controller.GetLastCommittedEntry()->GetURL());
#endif // OS_ANDROID
}
// Tests that the navigation entry that is marked as skippable on back/forward
// button does not get skipped for history.forward API calls.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
SetSkipOnBackDoNotSkipForHistoryForwardAPI) {
base::HistogramTester histograms;
GURL non_skippable_url(
embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_url));
GURL skippable_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(2, controller.GetCurrentEntryIndex());
EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
// Last entry should have been marked as skippable.
EXPECT_TRUE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(
"Navigation.BackForward.SetShouldSkipOnBackForwardUI", true, 1);
TestNavigationObserver nav_observer1(shell()->web_contents());
controller.GoToIndex(0);
nav_observer1.Wait();
EXPECT_EQ(non_skippable_url, controller.GetLastCommittedEntry()->GetURL());
// Attempt to go forward to the skippable entry using the History API should
// not skip the corresponding entry.
TestNavigationObserver nav_observer2(shell()->web_contents());
EXPECT_TRUE(ExecJs(root, "window.history.forward()"));
nav_observer2.Wait();
histograms.ExpectTotalCount("Navigation.BackForward.ForwardTargetSkipped", 0);
EXPECT_EQ(skippable_url, controller.GetLastCommittedEntry()->GetURL());
EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
}
// Tests that the oldest navigation entry that is marked as skippable is the one
// that is pruned if max entry count is reached.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
PruneOldestSkippableEntry) {
base::HistogramTester histograms;
// Set the max entry count as 3.
NavigationControllerImpl::set_max_entry_count_for_testing(3);
GURL non_skippable_url(
embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_url));
GURL skippable_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture.
GURL redirected_url(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(2, controller.GetCurrentEntryIndex());
EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
EXPECT_EQ(3, controller.GetEntryCount());
EXPECT_EQ(non_skippable_url, controller.GetEntryAtIndex(0)->GetURL());
EXPECT_EQ(skippable_url, controller.GetEntryAtIndex(1)->GetURL());
EXPECT_EQ(redirected_url, controller.GetEntryAtIndex(2)->GetURL());
// |skippable_url| entry should have been marked as skippable.
EXPECT_TRUE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(
"Navigation.BackForward.SetShouldSkipOnBackForwardUI", true, 1);
// A new navigation should lead to |skippable_url| to be pruned.
GURL new_navigation_url(embedded_test_server()->GetURL("/title3.html"));
EXPECT_TRUE(NavigateToURL(shell(), new_navigation_url));
// Should still have 3 entries.
EXPECT_EQ(3, controller.GetEntryCount());
EXPECT_EQ(non_skippable_url, controller.GetEntryAtIndex(0)->GetURL());
EXPECT_EQ(redirected_url, controller.GetEntryAtIndex(1)->GetURL());
EXPECT_EQ(new_navigation_url, controller.GetEntryAtIndex(2)->GetURL());
}
// Tests that we fallback to pruning the oldest entry if the last committed
// entry is the oldest skippable navigation entry.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
PruneOldestWhenLastCommittedIsSkippable) {
base::HistogramTester histograms;
// Set the max entry count as 2.
NavigationControllerImpl::set_max_entry_count_for_testing(2);
GURL non_skippable_url(
embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_url));
GURL skippable_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_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())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Navigate to a new same-site document from the renderer without a user
// gesture. This will mark |skippable_url| as skippable but since that is also
// the last committed entry, it will not be pruned. Instead the oldest entry
// will be removed.
GURL redirected_url(embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(
NavigateToURLFromRendererWithoutUserGesture(shell(), redirected_url));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(1, controller.GetCurrentEntryIndex());
EXPECT_EQ(1, controller.GetLastCommittedEntryIndex());
EXPECT_EQ(2, controller.GetEntryCount());
EXPECT_EQ(skippable_url, controller.GetEntryAtIndex(0)->GetURL());
EXPECT_EQ(redirected_url, controller.GetEntryAtIndex(1)->GetURL());
// |skippable_url| entry should have been marked as skippable.
EXPECT_TRUE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
EXPECT_FALSE(
controller.GetLastCommittedEntry()->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(
"Navigation.BackForward.SetShouldSkipOnBackForwardUI", true, 1);
}
// Tests that the navigation entry is marked as skippable on back/forward
// button if a subframe does a push state without ever getting a user
// activation.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
NoUserActivationSetSkipOnBackForwardSubframe) {
base::HistogramTester histograms;
GURL non_skippable_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_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())
->GetFrameTree()
->root();
GURL skippable_url(
embedded_test_server()->GetURL("/frame_tree/page_with_one_frame.html"));
EXPECT_TRUE(NavigateToURL(shell(), skippable_url));
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Invoke pushstate from a subframe.
std::string script = "history.pushState({}, 'page 1', 'simple_page_1.html')";
EXPECT_TRUE(
ExecJs(root->child_at(0), script, EXECUTE_SCRIPT_NO_USER_GESTURE));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(2, controller.GetCurrentEntryIndex());
EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
EXPECT_FALSE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
EXPECT_TRUE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(2)->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(
"Navigation.BackForward.SetShouldSkipOnBackForwardUI", true, 1);
EXPECT_TRUE(controller.CanGoBack());
// Attempt to go back or forward to the skippable entry should log the
// corresponding histogram and skip the corresponding entry.
TestNavigationObserver back_load_observer(shell()->web_contents());
controller.GoBack();
back_load_observer.Wait();
histograms.ExpectBucketCount("Navigation.BackForward.BackTargetSkipped", 1,
1);
EXPECT_EQ(non_skippable_url, controller.GetLastCommittedEntry()->GetURL());
EXPECT_EQ(0, controller.GetLastCommittedEntryIndex());
// Go forward to the 3rd entry.
TestNavigationObserver load_observer(shell()->web_contents());
controller.GoToIndex(2);
load_observer.Wait();
// A user gesture in the main frame now will lead to all same document
// entries to be marked as non-skippable.
root->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kNotifyActivation,
blink::mojom::UserActivationNotificationType::kTest);
EXPECT_TRUE(root->HasStickyUserActivation());
EXPECT_TRUE(root->HasTransientUserActivation());
EXPECT_FALSE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(2)->should_skip_on_back_forward_ui());
}
// Tests that the navigation entry is not marked as skippable on back/forward
// button if a subframe does a push state without ever getting a user
// activation on itself but there was a user gesture on the main frame.
IN_PROC_BROWSER_TEST_P(
NavigationControllerHistoryInterventionBrowserTest,
UserActivationMainFrameDoNotSetSkipOnBackForwardSubframe) {
base::HistogramTester histograms;
GURL non_skippable_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), non_skippable_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())
->GetFrameTree()
->root();
GURL url_with_frames(
embedded_test_server()->GetURL("/frame_tree/page_with_one_frame.html"));
EXPECT_TRUE(NavigateToURL(shell(), url_with_frames));
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
// Simulate user gesture in the main frame. Subframes creating entries without
// user gesture will not lead to the last committed entry being marked as
// skippable.
root->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kNotifyActivation,
blink::mojom::UserActivationNotificationType::kTest);
EXPECT_TRUE(root->HasStickyUserActivation());
EXPECT_TRUE(root->HasTransientUserActivation());
// Invoke pushstate from a subframe.
std::string script = "history.pushState({}, 'page 1', 'simple_page_1.html')";
EXPECT_TRUE(
ExecJs(root->child_at(0), script, EXECUTE_SCRIPT_NO_USER_GESTURE));
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
EXPECT_EQ(2, controller.GetCurrentEntryIndex());
EXPECT_EQ(2, controller.GetLastCommittedEntryIndex());
EXPECT_FALSE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(2)->should_skip_on_back_forward_ui());
histograms.ExpectBucketCount(
"Navigation.BackForward.SetShouldSkipOnBackForwardUI", true, 0);
}
// Tests that all same document entries are marked as skippable together.
IN_PROC_BROWSER_TEST_P(NavigationControllerHistoryInterventionBrowserTest,
SetSkipOnBackForwardSameDocumentEntries) {
// Consider the case:
// 1. [Z, A, (click), A#1, A#2, A#3, A#4, B]
// At this time all of A and A#1 through A#4 are non-skippable due to the
// click.
// 2. Let A#3 do a location.replace to another document
// [Z, A, A#1, A#2, Y, A#4, B]
// 3. Go to A#4, which is now the "current entry". All As are still
// non-skippable.
// 4. Let it now redirect without any user gesture to C.
// [Z, A, A#1, A#2, Y, A#4, C]
// At this time all of A entries should be marked as skippable.
// 5. Go back should skip A's and go to Z.
GURL z_url(embedded_test_server()->GetURL("/empty.html"));
EXPECT_TRUE(NavigateToURL(shell(), z_url));
GURL a_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), a_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
EXPECT_FALSE(root->HasStickyUserActivation());
EXPECT_FALSE(root->HasTransientUserActivation());
NavigationControllerImpl& controller = static_cast<NavigationControllerImpl&>(
shell()->web_contents()->GetController());
// Add the 2 pushstate entries. Note that ExecuteScript also sends a user
// gesture.
GURL a1_url(embedded_test_server()->GetURL("/title2.html"));
GURL a2_url(embedded_test_server()->GetURL("/title3.html"));
GURL a3_url(embedded_test_server()->GetURL(
"/navigation_controller/simple_page_1.html"));
GURL a4_url(embedded_test_server()->GetURL(
"/navigation_controller/simple_page_2.html"));
std::string script("history.pushState('', '','" + a1_url.spec() + "');");
ASSERT_TRUE(ExecJs(shell()->web_contents(), script));
script = "history.pushState('', '','" + a2_url.spec() + "');";
ASSERT_TRUE(ExecJs(shell()->web_contents(), script));
script = "history.pushState('', '','" + a3_url.spec() + "');";
ASSERT_TRUE(ExecJs(shell()->web_contents(), script));
script = "history.pushState('', '','" + a4_url.spec() + "');";
ASSERT_TRUE(ExecJs(shell()->web_contents(), script));
EXPECT_TRUE(root->HasStickyUserActivation());
EXPECT_TRUE(root->HasTransientUserActivation());
// None of the entries should be skippable.
EXPECT_EQ(6, controller.GetEntryCount());
EXPECT_FALSE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(2)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(3)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(4)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(5)->should_skip_on_back_forward_ui());
// Navigate to B.
GURL b_url(embedded_test_server()->GetURL("/empty.html"));
EXPECT_TRUE(NavigateToURLFromRenderer(shell(), b_url));
// Go back to a3_url and do location.replace.
TestNavigationObserver load_observer(shell()->web_contents());
controller.GoToOffset(-2);
load_observer.Wait();
EXPECT_EQ(a3_url, controller.GetLastCommittedEntry()->GetURL());
GURL y_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
ASSERT_TRUE(RendererLocationReplace(shell(), y_url));
EXPECT_EQ(7, controller.GetEntryCount());
EXPECT_FALSE(controller.GetEntryAtIndex(0)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(2)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(3)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(4)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(5)->should_skip_on_back_forward_ui());
EXPECT_FALSE(controller.GetEntryAtIndex(6)->should_skip_on_back_forward_ui());
// Go forward to a4_url.
{
TestNavigationObserver load_observer(shell()->web_contents());
controller.GoForward();
load_observer.Wait();
}
EXPECT_EQ(a4_url, controller.GetLastCommittedEntry()->GetURL());
// Redirect without user gesture to C.
GURL c_url(embedded_test_server()->GetURL("/frame_tree/top.html"));
EXPECT_TRUE(NavigateToURLFromRendererWithoutUserGesture(shell(), c_url));
// All entries belonging to A should be marked skippable.
EXPECT_EQ(7, controller.GetEntryCount());
EXPECT_EQ(a_url, controller.GetEntryAtIndex(1)->GetURL());
EXPECT_TRUE(controller.GetEntryAtIndex(1)->should_skip_on_back_forward_ui());
EXPECT_EQ(a1_url, controller.GetEntryAtIndex(2)->GetURL());
EXPECT_TRUE(controller.GetEntryAtIndex(2)->should_skip_on_back_forward_ui());
EXPECT_EQ(a2_url, controller.GetEntryAtIndex(3)->GetURL());
EXPECT_TRUE(controller.GetEntryAtIndex(3)->should_skip_on_back_forward_ui());
EXPECT_EQ(y_url, controller.GetEntryAtIndex(4)->GetURL());
EXPECT_FALSE(controller.GetEntryAtIndex(4)->should_skip_on_back_forward_ui());
EXPECT_EQ(a4_url, controller.GetEntryAtIndex(5)->GetURL());
EXPECT_TRUE(controller.GetEntryAtIndex(5)->should_skip_on_back_forward_ui());
EXPECT_EQ(c_url, controller.GetEntryAtIndex(6)->GetURL());
EXPECT_FALSE(controller.GetEntryAtIndex(6)->should_skip_on_back_forward_ui());
// Go back should skip all A entries and go to Y.
{
TestNavigationObserver load_observer(shell()->web_contents());
controller.GoBack();
load_observer.Wait();
}
EXPECT_EQ(y_url, controller.GetLastCommittedEntry()->GetURL());
// Going back again should skip all A entries and go to Z.
{
TestNavigationObserver load_observer(shell()->web_contents());
controller.GoBack();
load_observer.Wait();
}
EXPECT_EQ(z_url, controller.GetLastCommittedEntry()->GetURL());
}
// 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>");
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 SetUp() override {
feature_list_.InitAndEnableFeature(
features::kHistoryPreventSandboxedNavigation);
NavigationControllerBrowserTest::SetUp();
}
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())
->GetFrameTree()
->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())
->GetFrameTree()
->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(
{{features::kBackForwardCache,
{{"TimeToLiveInBackForwardCacheInSeconds", "3600"}}},
{features::kHistoryPreventSandboxedNavigation, {}}},
// Allow BackForwardCache for all devices regardless of their memory.
{features::kBackForwardCacheMemoryControls});
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()->GetMainFrame());
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())
->GetFrameTree()
->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 SetUp() override {
feature_list_.InitAndEnableFeature(
features::kHistoryPreventSandboxedNavigation);
NavigationControllerBrowserTest::SetUp();
}
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())
->GetFrameTree()
->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())
->GetFrameTree()
->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:
Shell* 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())
->GetFrameTree()
->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]
{
TestNavigationObserver navigation_observer(shell()->web_contents(), 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.
EXPECT_THAT(GetProcessedMainDocumentSequenceNumbers(),
ElementsAre(1, 1, 1, 2, 1, 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,
CallbackScriptRunner callback)
: DidCommitNavigationInterceptor(web_contents) {
callback_ = std::move(callback);
}
bool WillProcessDidCommitNavigation(
RenderFrameHost* render_frame_host,
NavigationRequest* navigation_request,
mojom::DidCommitProvisionalLoadParamsPtr* params,
mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
override {
std::move(callback_).Run();
return false;
}
private:
CallbackScriptRunner callback_;
};
} // namespace
// Test is flaky on Mac: https://crbug.com/1151545.
#if defined(OS_MAC)
#define MAYBE_CrossProcessIframeToInvalidURLCancelsRedirectSpoof \
DISABLED_CrossProcessIframeToInvalidURLCancelsRedirectSpoof
#else
#define MAYBE_CrossProcessIframeToInvalidURLCancelsRedirectSpoof \
CrossProcessIframeToInvalidURLCancelsRedirectSpoof
#endif
// 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,
MAYBE_CrossProcessIframeToInvalidURLCancelsRedirectSpoof) {
// This tests something that can only happened with out of process iframes.
if (!AreAllSitesIsolatedForTesting())
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())
->GetFrameTree()
->root()
->child_at(0);
DidCommitNavigationCanceller canceller(
shell()->web_contents(), base::BindLambdaForTesting([iframe]() {
EXPECT_TRUE(
ExecJs(iframe, "parent.location.href = 'chrome-guest://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->GetFrameTree()->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));
TestNavigationObserver load_observer(web_contents);
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 wait for DidStopLoading.
load_observer.Wait();
// 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())
->GetFrameTree()
->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()->GetFrameTree()->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()->GetFrameTree()->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 (except when RenderDocument
// subframe is on).
// TODO(http://crbug.com/1068965): Keep the history.state even with
// RenderDocument.
if (ShouldCreateNewHostForSameSiteSubframe()) {
EXPECT_EQ(nullptr, EvalJs(child, "history.state"));
} else {
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 (except when RenderDocument
// subframe is on).
// TODO(http://crbug.com/1068965): Keep the history.state even with
// RenderDocument.
if (ShouldCreateNewHostForSameSiteSubframe()) {
EXPECT_EQ(nullptr, EvalJs(child, "history.state"));
} else {
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 history.state value from before the failed navigation (except
// when RenderDocument subframe is on).
// TODO(http://crbug.com/1068965): Keep the history.state even with
// RenderDocument.
if (ShouldCreateNewHostForSameSiteSubframe()) {
EXPECT_EQ(nullptr, EvalJs(child, "history.state"));
} else {
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()->GetFrameTree()->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 (except when RenderDocument
// subframe is on).
// TODO(http://crbug.com/1068965): Keep the history.state even with
// RenderDocument.
if (ShouldCreateNewHostForSameSiteSubframe()) {
EXPECT_EQ(nullptr, EvalJs(child, "history.state"));
} else {
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())
->GetFrameTree()
->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 (except when RenderDocument
// subframe is on).
// TODO(http://crbug.com/1068965): Keep the history.state even with
// RenderDocument.
if (ShouldCreateNewHostForSameSiteSubframe()) {
EXPECT_EQ(nullptr, EvalJs(child, "history.state"));
} else {
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 (except when RenderDocument
// subframe is on).
// TODO(http://crbug.com/1068965): Keep the history.state even with
// RenderDocument.
if (ShouldCreateNewHostForSameSiteSubframe()) {
EXPECT_EQ(nullptr, EvalJs(child, "history.state"));
} else {
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 (except
// when RenderDocument subframe is on).
// TODO(http://crbug.com/1068965): Keep the history.state even with
// RenderDocument.
if (ShouldCreateNewHostForSameSiteSubframe()) {
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()->GetFrameTree()->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()->GetFrameTree()->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()->GetFrameTree()->root();
FrameTreeNode* ftn_b = contents()->GetFrameTree()->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()->GetFrameTree()->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());
// 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()->GetFrameTree()->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()->GetFrameTree()->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"));
}
// 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())->GetMainFrame();
// 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.GetOrigin(), 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).
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.GetOrigin(), 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.GetOrigin(), 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).
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.GetOrigin(), frame_entry_2->referrer().url);
EXPECT_EQ(network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin,
frame_entry_2->referrer().policy);
// TODO(http://crbug.com/1068965): Remove this when test passes.
if (ShouldCreateNewHostForSameSiteSubframe())
return;
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->GetMainFrame());
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(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.
navigation_manager.WaitForNavigationFinished();
EXPECT_FALSE(navigation);
}
INSTANTIATE_TEST_SUITE_P(All,
NavigationControllerAlertDialogBrowserTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()),
NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(All,
NavigationControllerBrowserTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()),
NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(All,
NavigationControllerBrowserTestNoServer,
testing::ValuesIn(RenderDocumentFeatureLevelValues()),
NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(All,
NavigationControllerDisableHistoryIntervention,
testing::ValuesIn(RenderDocumentFeatureLevelValues()),
NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(All,
NavigationControllerHistoryInterventionBrowserTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()),
NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(
All,
NavigationControllerMainDocumentSequenceNumberBrowserTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()),
NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(All,
RequestMonitoringNavigationBrowserTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()),
NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(All,
SandboxedNavigationControllerBrowserTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()),
NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(All,
SandboxedNavigationControllerWithBfcacheBrowserTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()),
NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(All,
SandboxedNavigationControllerPopupBrowserTest,
testing::ValuesIn(RenderDocumentFeatureLevelValues()),
NavigationControllerBrowserTest::DescribeParams);
INSTANTIATE_TEST_SUITE_P(
All,
InitialEmptyDocNavigationControllerBrowserTest,
testing::Combine(testing::ValuesIn(RenderDocumentFeatureLevelValues()),
testing::Bool()),
InitialEmptyDocNavigationControllerBrowserTest::DescribeParams);
} // namespace content