// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <fuchsia/element/cpp/fidl.h>
#include <fuchsia/ui/views/cpp/fidl.h>
#include <lib/zx/time.h>
#include "base/fuchsia/fuchsia_logging.h"
#include "base/fuchsia/mem_buffer_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "base/test/test_timeouts.h"
#include "build/build_config.h"
#include "components/fuchsia_component_support/annotations_manager.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/no_renderer_crashes_assertion.h"
#include "content/public/test/test_utils.h"
#include "fuchsia_web/common/string_util.h"
#include "fuchsia_web/common/test/fit_adapter.h"
#include "fuchsia_web/common/test/frame_for_test.h"
#include "fuchsia_web/common/test/frame_test_util.h"
#include "fuchsia_web/common/test/test_navigation_listener.h"
#include "fuchsia_web/webengine/browser/context_impl.h"
#include "fuchsia_web/webengine/browser/fake_semantics_manager.h"
#include "fuchsia_web/webengine/browser/frame_impl.h"
#include "fuchsia_web/webengine/browser/frame_impl_browser_test_base.h"
#include "net/test/embedded_test_server/request_handler_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/ozone/public/ozone_platform.h"
using testing::_;
using testing::AllOf;
using testing::AtLeast;
using testing::Contains;
using testing::Field;
using testing::InvokeWithoutArgs;
using testing::Key;
using testing::Mock;
using testing::Not;
// Use a shorter name for NavigationState, because it is referenced frequently
// in this file.
using NavigationDetails = fuchsia::web::NavigationState;
using OnNavigationStateChangedCallback =
fuchsia::web::NavigationEventListener::OnNavigationStateChangedCallback;
namespace {
constexpr char kPage1Path[] = "/title1.html";
constexpr char kPage2Path[] = "/title2.html";
constexpr char kPage3Path[] = "/websql.html";
constexpr char kReportCloseEventsPath[] = "/report_close_events.html";
constexpr char kVisibilityPath[] = "/visibility.html";
constexpr char kWaitSizePath[] = "/wait-size.html";
constexpr char kPage1Title[] = "title 1";
constexpr char kPage2Title[] = "title 2";
constexpr char kPage3Title[] = "websql not available";
constexpr char kDataUrl[] =
"data:text/html;base64,PGI+SGVsbG8sIHdvcmxkLi4uPC9iPg==";
MATCHER_P(NavigationHandleUrlEquals,
url,
"Checks equality with a NavigationHandle's URL.") {
return arg->GetURL() == url;
}
class MockWebContentsObserver : public content::WebContentsObserver {
public:
explicit MockWebContentsObserver(content::WebContents* web_contents) {
Observe(web_contents);
}
~MockWebContentsObserver() override = default;
MOCK_METHOD1(DidStartNavigation, void(content::NavigationHandle*));
MOCK_METHOD1(RenderViewDeleted,
void(content::RenderViewHost* render_view_host));
};
std::string GetDocumentVisibilityState(fuchsia::web::Frame* frame) {
auto visibility = base::MakeRefCounted<base::RefCountedData<std::string>>();
base::RunLoop loop;
frame->ExecuteJavaScript(
{"*"}, base::MemBufferFromString("document.visibilityState;", "test"),
[visibility, quit_loop = loop.QuitClosure()](
fuchsia::web::Frame_ExecuteJavaScript_Result result) {
ASSERT_TRUE(result.is_response());
visibility->data = *base::StringFromMemBuffer(result.response().result);
quit_loop.Run();
});
loop.Run();
return visibility->data;
}
} // namespace
// Defines a suite of tests that exercise Frame-level functionality, such as
// navigation commands and page events.
class FrameImplTest : public FrameImplTestBase {
public:
FrameImplTest() = default;
~FrameImplTest() override = default;
FrameImplTest(const FrameImplTest&) = delete;
FrameImplTest& operator=(const FrameImplTest&) = delete;
MOCK_METHOD1(OnServeHttpRequest,
void(const net::test_server::HttpRequest& request));
};
// Verifies that Frames are initially "hidden", changes to "visible" once the
// View is attached to a Presenter and back to "hidden" when the View is
// detached from the Presenter.
// TODO(crbug.com/42050537): Re-enable this test on Arm64 when femu is available
// for that architecture. This test requires Vulkan and Scenic to properly
// signal the Views visibility.
#if defined(ARCH_CPU_ARM_FAMILY)
#define MAYBE_VisibilityState DISABLED_VisibilityState
#else
#define MAYBE_VisibilityState VisibilityState
#endif
IN_PROC_BROWSER_TEST_F(FrameImplTest, MAYBE_VisibilityState) {
// This test uses the `fuchsia.ui.composition` variant of
// `Frame.CreateView*()`.
ASSERT_EQ(ui::OzonePlatform::GetPlatformNameForTest(), "flatland");
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
GURL page_url(embedded_test_server()->GetURL(kVisibilityPath));
auto frame = FrameForTest::Create(context(), {});
frame.ptr().set_error_handler([](zx_status_t status) {
ZX_LOG(ERROR, status);
ADD_FAILURE() << "Frame disconnected.";
});
base::RunLoop().RunUntilIdle();
// Navigate to a page and wait for it to finish loading.
ASSERT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
page_url.spec()));
frame.navigation_listener().RunUntilUrlEquals(page_url);
// Query the document.visibilityState before creating a View.
EXPECT_EQ(GetDocumentVisibilityState(frame.ptr().get()), "\"hidden\"");
// Query the document.visibilityState after creating the View, but without it
// actually "attached" to the view tree.
fuchsia::ui::views::ViewCreationToken view_token;
fuchsia::ui::views::ViewportCreationToken viewport_token;
auto status =
zx::channel::create(0, &viewport_token.value, &view_token.value);
ASSERT_EQ(ZX_OK, status);
fuchsia::web::CreateView2Args create_view_args;
create_view_args.set_view_creation_token(std::move(view_token));
frame->CreateView2(std::move(create_view_args));
base::RunLoop().RunUntilIdle();
EXPECT_EQ(GetDocumentVisibilityState(frame.ptr().get()), "\"hidden\"");
// Attach the View to a Presenter, the page should be visible.
auto annotations_manager =
std::make_unique<fuchsia_component_support::AnnotationsManager>();
fuchsia::element::AnnotationControllerHandle annotation_controller;
annotations_manager->Connect(annotation_controller.NewRequest());
auto presenter = base::ComponentContextForProcess()
->svc()
->Connect<::fuchsia::element::GraphicalPresenter>();
presenter.set_error_handler([](zx_status_t status) {
ZX_LOG(ERROR, status) << "GraphicalPresenter disconnected.";
ADD_FAILURE();
});
fuchsia::element::ViewSpec view_spec;
view_spec.set_viewport_creation_token(std::move(viewport_token));
view_spec.set_annotations({});
fuchsia::element::ViewControllerPtr view_controller;
presenter->PresentView(std::move(view_spec), std::move(annotation_controller),
view_controller.NewRequest(),
[](auto result) { EXPECT_FALSE(result.is_err()); });
frame.navigation_listener().RunUntilTitleEquals("visible");
// TODO(fxbug.dev/114431): Flatland does not support dismissing a view through
// the ViewController.
// Detach the ViewController, causing the View to be
// detached. This is a regression test for crbug.com/1141093, verifying that
// the page receives a "not visible" event as a result.
// view_controller->Dismiss();
// frame.navigation_listener().RunUntilTitleEquals("hidden");
}
// Verifies that the browser will navigate and generate a navigation listener
// event when LoadUrl() is called.
IN_PROC_BROWSER_TEST_F(FrameImplTest, NavigateFrame) {
auto frame = FrameForTest::Create(context(), {});
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
url::kAboutBlankURL));
frame.navigation_listener().RunUntilUrlAndTitleEquals(
GURL(url::kAboutBlankURL), url::kAboutBlankURL);
}
// Verifies that the browser does not crash if the Renderer process exits while
// a navigation event listener is attached to the Frame.
IN_PROC_BROWSER_TEST_F(FrameImplTest, NavigationListenerRendererProcessGone) {
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
GURL url(embedded_test_server()->GetURL("/nocontent"));
// Create a Frame and navigate it to a URL that will not "commit".
auto frame = FrameForTest::Create(context(), {});
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
url.spec()));
// Terminate the Renderer process and run the browser until that is observed.
content::RenderProcessHost* child_process =
context_impl()
->GetFrameImplForTest(&frame.ptr())
->web_contents()
->GetPrimaryMainFrame()
->GetProcess();
content::RenderProcessHostWatcher(
child_process, content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_READY)
.Wait();
ASSERT_TRUE(child_process->IsReady());
content::RenderProcessHostWatcher exit_observer(
child_process, content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
child_process->Shutdown(0);
exit_observer.Wait();
EXPECT_FALSE(exit_observer.did_exit_normally());
// Spin the browser main loop to flush any queued work, or FIDL activity.
base::RunLoop().RunUntilIdle();
}
// Verifies that the renderer process consumes more memory for document
// rendering.
IN_PROC_BROWSER_TEST_F(FrameImplTest, NavigationIncreasesMemoryUsage) {
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
GURL url(embedded_test_server()->GetURL(kPage1Path));
auto frame = FrameForTest::Create(context(), {});
// Get the renderer size when no renderer process is active.
base::test::TestFuture<uint64_t> before_nav_size;
frame->GetPrivateMemorySize(
CallbackToFitFunction(before_nav_size.GetCallback()));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
url.spec()));
frame.navigation_listener().RunUntilUrlAndTitleEquals(url, kPage1Title);
base::test::TestFuture<uint64_t> after_nav_size;
frame->GetPrivateMemorySize(
CallbackToFitFunction(after_nav_size.GetCallback()));
ASSERT_TRUE(after_nav_size.Wait());
EXPECT_EQ(before_nav_size.Get(), 0u); // No render process - zero bytes.
EXPECT_GT(after_nav_size.Get(), 0u);
}
IN_PROC_BROWSER_TEST_F(FrameImplTest, NavigateDataFrame) {
auto frame = FrameForTest::Create(context(), {});
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
kDataUrl));
frame.navigation_listener().RunUntilUrlAndTitleEquals(GURL(kDataUrl),
kDataUrl);
}
IN_PROC_BROWSER_TEST_F(FrameImplTest, FrameDeletedBeforeContext) {
auto frame = FrameForTest::Create(context(), {});
// Process the frame creation message.
base::RunLoop().RunUntilIdle();
FrameImpl* frame_impl = context_impl()->GetFrameImplForTest(&frame.ptr());
MockWebContentsObserver deletion_observer(frame_impl->web_contents());
base::RunLoop run_loop;
EXPECT_CALL(deletion_observer, RenderViewDeleted(_))
.WillOnce(InvokeWithoutArgs([&run_loop] { run_loop.Quit(); }));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
url::kAboutBlankURL));
frame.ptr().Unbind();
run_loop.Run();
// Check that |context| remains bound after the frame is closed.
EXPECT_TRUE(context());
}
IN_PROC_BROWSER_TEST_F(FrameImplTest, ContextDeletedBeforeFrame) {
auto frame = FrameForTest::Create(context(), {});
EXPECT_TRUE(frame.ptr());
base::RunLoop run_loop;
frame.ptr().set_error_handler([&run_loop](zx_status_t status) {
EXPECT_EQ(status, ZX_ERR_PEER_CLOSED);
run_loop.Quit();
});
context().Unbind();
run_loop.Run();
EXPECT_FALSE(frame.ptr());
}
IN_PROC_BROWSER_TEST_F(FrameImplTest, ContextDeletedBeforeFrameWithView) {
auto frame = FrameForTest::Create(context(), {});
EXPECT_TRUE(frame.ptr());
base::RunLoop().RunUntilIdle();
FrameImpl* frame_impl = context_impl()->GetFrameImplForTest(&frame.ptr());
fuchsia::ui::views::ViewCreationToken view_token;
fuchsia::ui::views::ViewportCreationToken viewport_token;
auto status =
zx::channel::create(0, &viewport_token.value, &view_token.value);
ZX_CHECK(status == ZX_OK, status);
fuchsia::web::CreateView2Args create_view_args;
create_view_args.set_view_creation_token(std::move(view_token));
frame->CreateView2(std::move(create_view_args));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(frame_impl->has_view_for_test());
base::RunLoop run_loop;
frame.ptr().set_error_handler([&run_loop](zx_status_t status) {
EXPECT_EQ(status, ZX_ERR_PEER_CLOSED);
run_loop.Quit();
});
context().Unbind();
run_loop.Run();
EXPECT_FALSE(frame.ptr());
}
// TODO(crbug.com/40507959): Remove this test when WebSQL is removed from
// Chrome.
IN_PROC_BROWSER_TEST_F(FrameImplTest, EnsureWebSqlDisabled) {
auto frame = FrameForTest::Create(context(), {});
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
GURL title3(embedded_test_server()->GetURL(kPage3Path));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
title3.spec()));
frame.navigation_listener().RunUntilUrlAndTitleEquals(title3, kPage3Title);
}
namespace {
void VerifyCanGoBackAndForward(FrameForTest& frame,
bool can_go_back_expected,
bool can_go_forward_expected) {
auto* state = frame.navigation_listener().current_state();
EXPECT_TRUE(state->has_can_go_back());
EXPECT_EQ(state->can_go_back(), can_go_back_expected);
EXPECT_TRUE(state->has_can_go_forward());
EXPECT_EQ(state->can_go_forward(), can_go_forward_expected);
}
} // namespace
IN_PROC_BROWSER_TEST_F(FrameImplTest, GoBackAndForward) {
auto frame = FrameForTest::Create(context(), {});
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
GURL title1(embedded_test_server()->GetURL(kPage1Path));
GURL title2(embedded_test_server()->GetURL(kPage2Path));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
title1.spec()));
frame.navigation_listener().RunUntilUrlTitleBackForwardEquals(
title1, kPage1Title, false, false);
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
title2.spec()));
frame.navigation_listener().RunUntilUrlTitleBackForwardEquals(
title2, kPage2Title, true, false);
frame.GetNavigationController()->GoBack();
frame.navigation_listener().RunUntilUrlTitleBackForwardEquals(
title1, kPage1Title, false, true);
// At the top of the navigation entry list; this should be a no-op.
frame.GetNavigationController()->GoBack();
// Process the navigation request message.
base::RunLoop().RunUntilIdle();
// Verify that the can-go-back/forward state has not changed.
VerifyCanGoBackAndForward(frame, false, true);
frame.GetNavigationController()->GoForward();
frame.navigation_listener().RunUntilUrlTitleBackForwardEquals(
title2, kPage2Title, true, false);
// At the end of the navigation entry list; this should be a no-op.
frame.GetNavigationController()->GoForward();
// Process the navigation request message.
base::RunLoop().RunUntilIdle();
// Back/forward state should not have changed, since the request was a
// no-op.
VerifyCanGoBackAndForward(frame, true, false);
}
namespace {
// An HTTP response stream whose response payload can be sent as "chunks"
// with indeterminate-length pauses in between.
class ChunkedHttpTransaction {
public:
explicit ChunkedHttpTransaction(
base::WeakPtr<net::test_server::HttpResponseDelegate> delegate)
: io_task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()),
send_state_(SendState::IDLE),
delegate_(delegate) {
EXPECT_FALSE(current_instance_);
current_instance_ = this;
}
ChunkedHttpTransaction(const ChunkedHttpTransaction&) = delete;
ChunkedHttpTransaction& operator=(const ChunkedHttpTransaction&) = delete;
static ChunkedHttpTransaction* current() {
EXPECT_TRUE(current_instance_);
return current_instance_;
}
void Close() {
EnsureSendCompleted();
io_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&net::test_server::HttpResponseDelegate::FinishResponse,
delegate_));
delete this;
}
void EnsureSendCompleted() {
if (send_state_ == SendState::IDLE) {
return;
}
base::RunLoop run_loop;
send_chunk_complete_callback_ = run_loop.QuitClosure();
run_loop.Run();
EXPECT_EQ(send_state_, SendState::IDLE);
}
void SendChunk(const std::string& chunk) {
EnsureSendCompleted();
send_state_ = SendState::BLOCKED;
io_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
&net::test_server::HttpResponseDelegate::SendContents, delegate_,
chunk,
base::BindOnce(&ChunkedHttpTransaction::SendChunkCompleteOnIoThread,
base::Unretained(this),
base::SingleThreadTaskRunner::GetCurrentDefault())));
}
private:
static ChunkedHttpTransaction* current_instance_;
~ChunkedHttpTransaction() { current_instance_ = nullptr; }
void SendChunkCompleteOnIoThread(
scoped_refptr<base::TaskRunner> ui_thread_task_runner) {
ui_thread_task_runner->PostTask(
FROM_HERE,
base::BindOnce(&ChunkedHttpTransaction::SendChunkCompleteOnUiThread,
base::Unretained(this)));
}
void SendChunkCompleteOnUiThread() {
send_state_ = SendState::IDLE;
if (send_chunk_complete_callback_) {
std::move(send_chunk_complete_callback_).Run();
}
}
scoped_refptr<base::SingleThreadTaskRunner> io_task_runner_;
// Set by callers to SendChunk() waiting for the previous chunk to complete.
base::OnceClosure send_chunk_complete_callback_;
enum SendState { IDLE, BLOCKED };
SendState send_state_;
base::WeakPtr<net::test_server::HttpResponseDelegate> delegate_;
};
ChunkedHttpTransaction* ChunkedHttpTransaction::current_instance_ = nullptr;
class ChunkedHttpTransactionFactory : public net::test_server::HttpResponse {
public:
ChunkedHttpTransactionFactory() = default;
ChunkedHttpTransactionFactory(const ChunkedHttpTransactionFactory&) = delete;
ChunkedHttpTransactionFactory& operator=(
const ChunkedHttpTransactionFactory&) = delete;
~ChunkedHttpTransactionFactory() override = default;
void SetOnResponseCreatedCallback(base::OnceClosure on_response_created) {
on_response_created_ = std::move(on_response_created);
}
// net::test_server::HttpResponse implementation.
void SendResponse(
base::WeakPtr<net::test_server::HttpResponseDelegate> delegate) override {
// The ChunkedHttpTransaction manages its own lifetime.
new ChunkedHttpTransaction(delegate);
if (on_response_created_) {
std::move(on_response_created_).Run();
}
}
private:
base::OnceClosure on_response_created_;
};
} // namespace
IN_PROC_BROWSER_TEST_F(FrameImplTest, NavigationEventDuringPendingLoad) {
auto frame = FrameForTest::Create(context(), {});
auto factory = std::make_unique<ChunkedHttpTransactionFactory>();
base::RunLoop transaction_created_run_loop;
factory->SetOnResponseCreatedCallback(
transaction_created_run_loop.QuitClosure());
embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
&net::test_server::HandlePrefixedRequest, "/pausable",
base::BindLambdaForTesting(
[&](const net::test_server::HttpRequest&)
-> std::unique_ptr<net::test_server::HttpResponse> {
CHECK(factory);
return std::move(factory);
})));
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
GURL hung_url(embedded_test_server()->GetURL("/pausable"));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
hung_url.spec()));
fuchsia::web::NavigationState state_change;
state_change.set_url(hung_url.spec());
state_change.set_is_main_document_loaded(false);
transaction_created_run_loop.Run();
ChunkedHttpTransaction* transaction = ChunkedHttpTransaction::current();
transaction->SendChunk(
"HTTP/1.0 200 OK\r\n"
"Host: localhost\r\n"
"Content-Type: text/html\r\n\r\n"
"<html><head><title>initial load</title>");
state_change.set_title("initial load");
state_change.set_is_main_document_loaded(false);
frame.navigation_listener().RunUntilNavigationStateMatches(state_change);
transaction->SendChunk(
"<script>document.title='final load';</script><body></body>");
transaction->Close();
state_change.set_title("final load");
state_change.set_is_main_document_loaded(true);
frame.navigation_listener().RunUntilNavigationStateMatches(state_change);
}
IN_PROC_BROWSER_TEST_F(FrameImplTest, ReloadFrame) {
auto frame = FrameForTest::Create(context(), {});
embedded_test_server()->RegisterRequestMonitor(base::BindRepeating(
&FrameImplTest::OnServeHttpRequest, base::Unretained(this)));
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
GURL url(embedded_test_server()->GetURL(kPage1Path));
EXPECT_CALL(*this, OnServeHttpRequest(_)).Times(AtLeast(1));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
url.spec()));
frame.navigation_listener().RunUntilUrlAndTitleEquals(url, kPage1Title);
// Reload with NO_CACHE.
{
MockWebContentsObserver web_contents_observer(
context_impl()->GetFrameImplForTest(&frame.ptr())->web_contents_.get());
base::RunLoop run_loop;
EXPECT_CALL(web_contents_observer,
DidStartNavigation(NavigationHandleUrlEquals(url)))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
frame.GetNavigationController()->Reload(fuchsia::web::ReloadType::NO_CACHE);
run_loop.Run();
}
// Reload with PARTIAL_CACHE.
{
MockWebContentsObserver web_contents_observer(
context_impl()->GetFrameImplForTest(&frame.ptr())->web_contents_.get());
base::RunLoop run_loop;
EXPECT_CALL(web_contents_observer,
DidStartNavigation(NavigationHandleUrlEquals(url)))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
frame.GetNavigationController()->Reload(
fuchsia::web::ReloadType::PARTIAL_CACHE);
run_loop.Run();
}
}
// Verifies that NavigationState correctly reports when the Renderer terminates
// or crashes.
IN_PROC_BROWSER_TEST_F(FrameImplTest, NavigationState_RendererGone) {
auto frame = FrameForTest::Create(context(), {});
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
GURL url(embedded_test_server()->GetURL(kPage1Path));
// Navigate to a page.
ASSERT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
url.spec()));
frame.navigation_listener().RunUntilUrlAndTitleEquals(url, kPage1Title);
// Kill the renderer for the tab.
auto* frame_impl = context_impl()->GetFrameImplForTest(&frame.ptr());
auto* web_contents = frame_impl->web_contents_for_test();
{
content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes;
content::RenderFrameDeletedObserver crash_observer(
web_contents->GetPrimaryMainFrame());
web_contents->GetPrimaryMainFrame()->GetProcess()->Shutdown(1);
crash_observer.WaitUntilDeleted();
}
// Wait for the NavigationListener to also observe the transition.
fuchsia::web::NavigationState error_state;
error_state.set_page_type(fuchsia::web::PageType::ERROR);
frame.navigation_listener().RunUntilNavigationStateMatches(error_state);
const fuchsia::web::NavigationState* current_state =
frame.navigation_listener().current_state();
ASSERT_TRUE(current_state->has_url());
EXPECT_EQ(current_state->url(), url.spec());
ASSERT_TRUE(current_state->has_title());
EXPECT_EQ(current_state->title(), kPage1Title);
ASSERT_TRUE(current_state->has_page_type());
EXPECT_EQ(current_state->page_type(), fuchsia::web::PageType::ERROR);
}
IN_PROC_BROWSER_TEST_F(FrameImplTest, NoNavigationObserverAttached) {
auto frame = FrameForTest::Create(context(), {});
base::RunLoop().RunUntilIdle();
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
GURL title1(embedded_test_server()->GetURL(kPage1Path));
GURL title2(embedded_test_server()->GetURL(kPage2Path));
MockWebContentsObserver observer(
context_impl()->GetFrameImplForTest(&frame.ptr())->web_contents_.get());
{
base::RunLoop run_loop;
EXPECT_CALL(observer, DidStartNavigation(NavigationHandleUrlEquals(title1)))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
title1.spec()));
run_loop.Run();
}
{
base::RunLoop run_loop;
EXPECT_CALL(observer, DidStartNavigation(NavigationHandleUrlEquals(title2)))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
title2.spec()));
run_loop.Run();
}
}
// Verifies that a Frame will handle navigation listener disconnection events
// gracefully.
IN_PROC_BROWSER_TEST_F(FrameImplTest, NavigationObserverDisconnected) {
auto frame = FrameForTest::Create(context(), {});
base::RunLoop().RunUntilIdle();
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
GURL title1(embedded_test_server()->GetURL(kPage1Path));
GURL title2(embedded_test_server()->GetURL(kPage2Path));
MockWebContentsObserver web_contents_observer(
context_impl()->GetFrameImplForTest(&frame.ptr())->web_contents_.get());
EXPECT_CALL(web_contents_observer,
DidStartNavigation(NavigationHandleUrlEquals(title1)));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
title1.spec()));
frame.navigation_listener().RunUntilUrlAndTitleEquals(title1, kPage1Title);
// Disconnect the listener & spin the runloop to propagate the disconnection
// event over IPC.
frame.navigation_listener_binding().Close(ZX_ERR_PEER_CLOSED);
base::RunLoop().RunUntilIdle();
base::RunLoop run_loop;
EXPECT_CALL(web_contents_observer,
DidStartNavigation(NavigationHandleUrlEquals(title2)))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
title2.spec()));
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(FrameImplTest, DelayedNavigationEventAck) {
auto frame = FrameForTest::Create(context(), {});
base::RunLoop().RunUntilIdle();
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
GURL title1(embedded_test_server()->GetURL(kPage1Path));
GURL title2(embedded_test_server()->GetURL(kPage2Path));
// Expect an navigation event here, but deliberately postpone acknowledgement
// until the end of the test.
OnNavigationStateChangedCallback captured_ack_cb;
frame.navigation_listener().SetBeforeAckHook(base::BindRepeating(
[](OnNavigationStateChangedCallback* dest_cb,
const fuchsia::web::NavigationState& state,
OnNavigationStateChangedCallback cb) { *dest_cb = std::move(cb); },
base::Unretained(&captured_ack_cb)));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
title1.spec()));
fuchsia::web::NavigationState expected_state;
expected_state.set_url(title1.spec());
frame.navigation_listener().RunUntilNavigationStateMatches(expected_state);
EXPECT_TRUE(captured_ack_cb);
frame.navigation_listener().SetBeforeAckHook({});
// Navigate to a second page.
{
// Since we have blocked NavigationEventObserver's flow, we must observe the
// lower level browser navigation events directly from the WebContents.
MockWebContentsObserver web_contents_observer(
context_impl()->GetFrameImplForTest(&frame.ptr())->web_contents_.get());
base::RunLoop run_loop;
EXPECT_CALL(web_contents_observer,
DidStartNavigation(NavigationHandleUrlEquals(title2)))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
title2.spec()));
run_loop.Run();
}
// Navigate to the first page.
{
MockWebContentsObserver web_contents_observer(
context_impl()->GetFrameImplForTest(&frame.ptr())->web_contents_.get());
base::RunLoop run_loop;
EXPECT_CALL(web_contents_observer,
DidStartNavigation(NavigationHandleUrlEquals(title1)))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
title1.spec()));
run_loop.Run();
}
// Since there was no observable change in navigation state since the last
// ack, there should be no more NavigationEvents generated.
captured_ack_cb();
frame.navigation_listener().RunUntilUrlAndTitleEquals(title1, kPage1Title);
}
namespace {
// Observes events specific to the Stop() test case.
struct WebContentsObserverForStop : public content::WebContentsObserver {
using content::WebContentsObserver::Observe;
MOCK_METHOD1(DidStartNavigation, void(content::NavigationHandle*));
MOCK_METHOD0(NavigationStopped, void());
};
} // namespace
IN_PROC_BROWSER_TEST_F(FrameImplTest, Stop) {
auto frame = FrameForTest::Create(context(), {});
base::RunLoop().RunUntilIdle();
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
// Use a request handler that will accept the connection and stall
// indefinitely.
GURL hung_url(embedded_test_server()->GetURL("/hung"));
{
base::RunLoop run_loop;
WebContentsObserverForStop observer;
observer.Observe(
context_impl()->GetFrameImplForTest(&frame.ptr())->web_contents_.get());
EXPECT_CALL(observer, DidStartNavigation(_))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
frame.GetNavigationController()->LoadUrl(
hung_url.spec(), fuchsia::web::LoadUrlParams(),
[](fuchsia::web::NavigationController_LoadUrl_Result) {});
run_loop.Run();
}
EXPECT_TRUE(context_impl()
->GetFrameImplForTest(&frame.ptr())
->web_contents_->IsLoading());
{
base::RunLoop run_loop;
WebContentsObserverForStop observer;
observer.Observe(
context_impl()->GetFrameImplForTest(&frame.ptr())->web_contents_.get());
EXPECT_CALL(observer, NavigationStopped())
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
frame.GetNavigationController()->Stop();
run_loop.Run();
}
EXPECT_FALSE(context_impl()
->GetFrameImplForTest(&frame.ptr())
->web_contents_->IsLoading());
}
// TODO(crbug.com/42050537): Enable on ARM64 when bots support Vulkan.
// This test requires Vulkan and Scenic to properly signal the Views visibility.
#if defined(ARCH_CPU_ARM_FAMILY)
#define MAYBE_SetPageScale DISABLED_SetPageScale
#else
// TODO(crbug.com/42050328): SetPageScale/ExecuteJavaScript is racey, causing
// the test to flake.
#define MAYBE_SetPageScale DISABLED_SetPageScale
#endif
IN_PROC_BROWSER_TEST_F(FrameImplTest, MAYBE_SetPageScale) {
ASSERT_EQ(ui::OzonePlatform::GetInstance()->GetPlatformNameForTest(),
"flatland");
auto frame = FrameForTest::Create(context(), {});
fuchsia::ui::views::ViewCreationToken view_token;
fuchsia::ui::views::ViewportCreationToken viewport_token;
auto status =
zx::channel::create(0, &viewport_token.value, &view_token.value);
ZX_CHECK(status == ZX_OK, status);
fuchsia::web::CreateView2Args create_view_args;
create_view_args.set_view_creation_token(std::move(view_token));
frame->CreateView2(std::move(create_view_args));
// Attach the View to a Presenter, the page should be visible.
auto presenter = base::ComponentContextForProcess()
->svc()
->Connect<::fuchsia::element::GraphicalPresenter>();
presenter.set_error_handler([](zx_status_t status) {
ZX_LOG(ERROR, status) << "GraphicalPresenter disconnected.";
ADD_FAILURE();
});
::fuchsia::element::ViewSpec view_spec;
view_spec.set_viewport_creation_token(std::move(viewport_token));
view_spec.set_annotations({});
::fuchsia::element::ViewControllerPtr view_controller;
presenter->PresentView(std::move(view_spec), nullptr,
view_controller.NewRequest(),
[](auto result) { EXPECT_FALSE(result.is_err()); });
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
GURL url = embedded_test_server()->GetURL(kWaitSizePath);
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
url.spec()));
frame.navigation_listener().RunUntilUrlAndTitleEquals(url, "done");
std::optional<base::Value> default_dpr =
ExecuteJavaScript(frame.ptr().get(), "window.devicePixelRatio");
ASSERT_TRUE(default_dpr);
EXPECT_EQ(default_dpr->GetDouble(), 1.0f);
// Update scale and verify that devicePixelRatio is updated accordingly.
const float kZoomInScale = 1.5;
fuchsia::web::ContentAreaSettings settings;
settings.set_page_scale(kZoomInScale);
frame->SetContentAreaSettings(std::move(settings));
std::optional<base::Value> scaled_dpr =
ExecuteJavaScript(frame.ptr().get(), "window.devicePixelRatio");
ASSERT_TRUE(scaled_dpr);
EXPECT_EQ(scaled_dpr->GetDouble(), kZoomInScale);
// Navigate to the same page on http://localhost. This is a different site,
// so it will be loaded in a new renderer process. Page scale value should be
// preserved.
GURL url2 = embedded_test_server()->GetURL("localhost", kWaitSizePath);
EXPECT_NE(url.host(), url2.host());
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
url2.spec()));
frame.navigation_listener().RunUntilUrlAndTitleEquals(url2, "done");
std::optional<base::Value> dpr_after_navigation =
ExecuteJavaScript(frame.ptr().get(), "window.devicePixelRatio");
ASSERT_TRUE(scaled_dpr);
EXPECT_EQ(dpr_after_navigation, scaled_dpr);
// Reset the scale to 1.0 (default) and verify that reported DPR is updated
// to 1.0.
const float kDefaultScale = 1.0;
fuchsia::web::ContentAreaSettings settings2;
settings2.set_page_scale(kDefaultScale);
frame->SetContentAreaSettings(std::move(settings2));
std::optional<base::Value> dpr_after_reset =
ExecuteJavaScript(frame.ptr().get(), "window.devicePixelRatio");
ASSERT_TRUE(dpr_after_reset);
EXPECT_EQ(dpr_after_reset->GetDouble(), kDefaultScale);
// Zoom out by setting scale to 0.5.
const float kZoomOutScale = 0.5;
fuchsia::web::ContentAreaSettings settings3;
settings3.set_page_scale(kZoomOutScale);
frame->SetContentAreaSettings(std::move(settings3));
std::optional<base::Value> zoomed_out_dpr =
ExecuteJavaScript(frame.ptr().get(), "window.devicePixelRatio");
ASSERT_TRUE(zoomed_out_dpr);
EXPECT_EQ(zoomed_out_dpr->GetDouble(), kZoomOutScale);
// Create another frame. Verify that the scale factor is not applied to the
// new frame.
auto frame2 = FrameForTest::Create(context(), {});
status = zx::channel::create(0, &viewport_token.value, &view_token.value);
ZX_CHECK(status == ZX_OK, status);
create_view_args.set_view_creation_token(std::move(view_token));
frame2->CreateView2(std::move(create_view_args));
view_spec.set_viewport_creation_token(std::move(viewport_token));
view_spec.set_annotations({});
presenter->PresentView(std::move(view_spec), nullptr,
view_controller.NewRequest(), [](auto) {});
EXPECT_TRUE(LoadUrlAndExpectResponse(frame2.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
url.spec()));
frame2.navigation_listener().RunUntilUrlAndTitleEquals(url, "done");
std::optional<base::Value> frame2_dpr =
ExecuteJavaScript(frame2.ptr().get(), "window.devicePixelRatio");
ASSERT_TRUE(frame2_dpr);
EXPECT_EQ(frame2_dpr->GetDouble(), 1.0);
}
IN_PROC_BROWSER_TEST_F(FrameImplTest, RecreateView) {
auto frame = FrameForTest::Create(context(), {});
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
// Process the Frame creation request, and verify we can get the FrameImpl.
base::RunLoop().RunUntilIdle();
FrameImpl* frame_impl = context_impl()->GetFrameImplForTest(&frame.ptr());
ASSERT_TRUE(frame_impl);
EXPECT_FALSE(frame_impl->has_view_for_test());
// Verify that the Frame can navigate, prior to the View being created.
const GURL page1_url(embedded_test_server()->GetURL(kPage1Path));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
page1_url.spec()));
frame.navigation_listener().RunUntilUrlAndTitleEquals(page1_url, kPage1Title);
// Request a View from the Frame, and pump the loop to process the request.
fuchsia::ui::views::ViewCreationToken view_token;
fuchsia::ui::views::ViewportCreationToken viewport_token;
auto status =
zx::channel::create(0, &viewport_token.value, &view_token.value);
ZX_CHECK(status == ZX_OK, status);
fuchsia::web::CreateView2Args create_view_args;
create_view_args.set_view_creation_token(std::move(view_token));
frame->CreateView2(std::move(create_view_args));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(frame_impl->has_view_for_test());
// Verify that the Frame still works, by navigating to Page #2.
const GURL page2_url(embedded_test_server()->GetURL(kPage2Path));
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
page2_url.spec()));
frame.navigation_listener().RunUntilUrlAndTitleEquals(page2_url, kPage2Title);
// Create new View tokens and request a new view.
status = zx::channel::create(0, &viewport_token.value, &view_token.value);
ZX_CHECK(status == ZX_OK, status);
create_view_args.set_view_creation_token(std::move(view_token));
frame->CreateView2(std::move(create_view_args));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(frame_impl->has_view_for_test());
// Verify that the Frame still works, by navigating back to Page #1.
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
page1_url.spec()));
frame.navigation_listener().RunUntilUrlAndTitleEquals(page1_url, kPage1Title);
}
IN_PROC_BROWSER_TEST_F(FrameImplTest, CreateViewMissingArgs) {
auto frame = FrameForTest::Create(context(), {});
// Close the NavigationEventListener to avoid a test failure resulting, when
// it is disconnected as a result of the Frame closing.
frame.navigation_listener_binding().Close(ZX_OK);
// Create a view with GFX, without supplying a valid view token.
base::test::TestFuture<zx_status_t> frame_status;
frame.ptr().set_error_handler(
CallbackToFitFunction(frame_status.GetCallback()));
frame->CreateView({});
EXPECT_EQ(frame_status.Get(), ZX_ERR_INVALID_ARGS);
}
IN_PROC_BROWSER_TEST_F(FrameImplTest, CreateView2MissingArgs) {
auto frame = FrameForTest::Create(context(), {});
// Close the NavigationEventListener to avoid a test failure resulting, when
// it is disconnected as a result of the Frame closing.
frame.navigation_listener_binding().Close(ZX_OK);
// Create a view with GFX, without supplying a valid view token.
base::test::TestFuture<zx_status_t> frame_status;
frame.ptr().set_error_handler(
CallbackToFitFunction(frame_status.GetCallback()));
frame->CreateView2({});
EXPECT_EQ(frame_status.Get(), ZX_ERR_INVALID_ARGS);
}
IN_PROC_BROWSER_TEST_F(FrameImplTest, ChildFrameNavigationIgnored) {
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
GURL page_url(embedded_test_server()->GetURL("/creates_child_frame.html"));
// Navigate to a page and wait for the navigation to complete.
auto frame = FrameForTest::Create(context(), {});
fuchsia::web::NavigationControllerPtr controller;
frame->GetNavigationController(controller.NewRequest());
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
page_url.spec()));
fuchsia::web::NavigationState expected_state;
expected_state.set_url(page_url.spec());
expected_state.set_title("main frame");
expected_state.set_is_main_document_loaded(true);
frame.navigation_listener().RunUntilNavigationStateMatches(
std::move(expected_state));
// Notify the page so that it constructs a child iframe.
fuchsia::web::WebMessage message;
message.set_data(base::MemBufferFromString("test", "test"));
base::test::TestFuture<fuchsia::web::Frame_PostMessage_Result> post_result;
frame->PostMessage(embedded_test_server()->GetOrigin().Serialize(),
std::move(message),
CallbackToFitFunction(post_result.GetCallback()));
frame.navigation_listener().SetBeforeAckHook(
base::BindRepeating([](const fuchsia::web::NavigationState& change,
OnNavigationStateChangedCallback callback) {
// The child iframe's loading status should not affect the
// is_main_document_loaded() bit.
if (change.has_is_main_document_loaded()) {
ADD_FAILURE();
}
callback();
}));
frame.navigation_listener().RunUntilUrlAndTitleEquals(page_url,
"iframe loaded");
}
// Tests SetNavigationEventListener() immediately returns a NavigationEvent,
// even in the absence of a new navigation.
IN_PROC_BROWSER_TEST_F(FrameImplTest, ImmediateNavigationEvent) {
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
GURL page_url(embedded_test_server()->GetURL(kPage1Path));
// The first NavigationState received should be empty.
base::RunLoop run_loop;
auto frame = FrameForTest::Create(context(), {});
frame.navigation_listener().SetBeforeAckHook(base::BindRepeating(
[](base::OnceClosure quit_loop,
const fuchsia::web::NavigationState& change,
OnNavigationStateChangedCallback callback) {
std::move(quit_loop).Run();
EXPECT_TRUE(change.IsEmpty());
callback();
},
run_loop.QuitClosure()));
run_loop.Run();
frame.navigation_listener().SetBeforeAckHook({});
// Navigate to a page and wait for the navigation to complete.
EXPECT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
fuchsia::web::LoadUrlParams(),
page_url.spec()));
frame.navigation_listener().RunUntilUrlEquals(page_url);
// Attach a new navigation listener, we should get the new page state, even if
// no new navigation occurred.
frame.CreateAndAttachNavigationListener(/*flags=*/{});
frame.navigation_listener().RunUntilUrlAndTitleEquals(page_url, kPage1Title);
}
// Check loading an invalid URL in NavigationController.LoadUrl() sets the right
// error.
IN_PROC_BROWSER_TEST_F(FrameImplTest, InvalidUrl) {
auto frame = FrameForTest::Create(context(), {});
base::test::TestFuture<fuchsia::web::NavigationController_LoadUrl_Result>
result;
auto controller = frame.GetNavigationController();
controller->LoadUrl("http:google.com:foo", fuchsia::web::LoadUrlParams(),
CallbackToFitFunction(result.GetCallback()));
ASSERT_TRUE(result.Wait());
ASSERT_TRUE(result.Get().is_err());
EXPECT_EQ(result.Get().err(),
fuchsia::web::NavigationControllerError::INVALID_URL);
}
// Check setting invalid headers in NavigationController.LoadUrl() sets the
// right error.
IN_PROC_BROWSER_TEST_F(FrameImplTest, InvalidHeader) {
auto frame = FrameForTest::Create(context(), {});
{
// Set an invalid header name.
fuchsia::web::LoadUrlParams load_url_params;
fuchsia::net::http::Header header;
header.name = StringToBytes("Invalid:Header");
header.value = StringToBytes("1");
load_url_params.set_headers({header});
base::test::TestFuture<fuchsia::web::NavigationController_LoadUrl_Result>
result;
auto controller = frame.GetNavigationController();
controller->LoadUrl("http://site.ext/", std::move(load_url_params),
CallbackToFitFunction(result.GetCallback()));
ASSERT_TRUE(result.Wait());
ASSERT_TRUE(result.Get().is_err());
EXPECT_EQ(result.Get().err(),
fuchsia::web::NavigationControllerError::INVALID_HEADER);
}
{
// Set an invalid header value.
fuchsia::web::LoadUrlParams load_url_params;
fuchsia::net::http::Header header;
header.name = StringToBytes("Header");
header.value = StringToBytes("Invalid\rValue");
load_url_params.set_headers({header});
base::test::TestFuture<fuchsia::web::NavigationController_LoadUrl_Result>
result;
auto controller = frame.GetNavigationController();
controller->LoadUrl("http://site.ext/", std::move(load_url_params),
CallbackToFitFunction(result.GetCallback()));
ASSERT_TRUE(result.Wait());
ASSERT_TRUE(result.Get().is_err());
EXPECT_EQ(result.Get().err(),
fuchsia::web::NavigationControllerError::INVALID_HEADER);
}
}
IN_PROC_BROWSER_TEST_F(FrameImplTest, ContentAreaSettings) {
auto frame = FrameForTest::Create(context(), {});
base::RunLoop().RunUntilIdle();
auto* frame_impl = context_impl()->GetFrameImplForTest(&frame.ptr());
auto* web_contents = frame_impl->web_contents_for_test();
// Frame should start with default values in web_contents.
{
blink::web_pref::WebPreferences web_prefs =
web_contents->GetOrCreateWebPreferences();
EXPECT_FALSE(web_prefs.hide_scrollbars);
EXPECT_EQ(web_prefs.autoplay_policy,
blink::mojom::AutoplayPolicy::kDocumentUserActivationRequired);
EXPECT_EQ(web_prefs.preferred_color_scheme,
blink::mojom::PreferredColorScheme::kLight);
}
// SetContentAreaSettings with empty settings object should not change any
// existing settings.
{
fuchsia::web::ContentAreaSettings settings;
frame->SetContentAreaSettings(std::move(settings));
base::RunLoop().RunUntilIdle();
blink::web_pref::WebPreferences web_prefs =
web_contents->GetOrCreateWebPreferences();
EXPECT_FALSE(web_prefs.hide_scrollbars);
EXPECT_EQ(web_prefs.autoplay_policy,
blink::mojom::AutoplayPolicy::kDocumentUserActivationRequired);
EXPECT_EQ(web_prefs.preferred_color_scheme,
blink::mojom::PreferredColorScheme::kLight);
}
// Set hide_scrollbars field and expect that it's reflected in web_contents.
// Expect other fields to be unchanged.
{
fuchsia::web::ContentAreaSettings settings;
settings.set_hide_scrollbars(true);
frame->SetContentAreaSettings(std::move(settings));
base::RunLoop().RunUntilIdle();
blink::web_pref::WebPreferences web_prefs =
web_contents->GetOrCreateWebPreferences();
EXPECT_TRUE(web_prefs.hide_scrollbars);
EXPECT_EQ(web_prefs.autoplay_policy,
blink::mojom::AutoplayPolicy::kDocumentUserActivationRequired);
EXPECT_EQ(web_prefs.preferred_color_scheme,
blink::mojom::PreferredColorScheme::kLight);
}
// ResetContentAreaSettings should revert preferences to their default values
// in web_contents.
{
frame->ResetContentAreaSettings();
base::RunLoop().RunUntilIdle();
blink::web_pref::WebPreferences web_prefs =
web_contents->GetOrCreateWebPreferences();
EXPECT_FALSE(web_prefs.hide_scrollbars);
EXPECT_EQ(web_prefs.autoplay_policy,
blink::mojom::AutoplayPolicy::kDocumentUserActivationRequired);
EXPECT_EQ(web_prefs.preferred_color_scheme,
blink::mojom::PreferredColorScheme::kLight);
}
}
// Verifies that a `Frame` closes correctly if `Close()` is called before any
// page is loaded.
IN_PROC_BROWSER_TEST_F(FrameImplTest, Close_BeforeNavigation) {
auto frame = FrameForTest::Create(context(), {});
base::RunLoop loop;
frame.ptr().set_error_handler(
[quit = loop.QuitClosure()](zx_status_t status) {
EXPECT_EQ(status, ZX_OK);
std::move(quit).Run();
});
// Request to gracefully close the Frame, which should be immediate since
// nothing has been loaded into it yet.
frame->Close({});
loop.Run();
}
namespace {
// Helper class for `Frame.Close()` tests, that navigates the `Frame` to an
// event-recording page, and connects to accumulate the list of events it
// receives.
// If `hang_on_event` is true then the web page will be configured to busy-loop
// as soon as any event is received, to allow timeouts to be simulated.
class FrameForTestWithMessageLog : public FrameForTest {
public:
FrameForTestWithMessageLog(FrameForTest frame,
net::EmbeddedTestServer& test_server,
bool hang_on_event = false)
: FrameForTest(std::move(frame)) {
// Navigate to a page that will record the relevant events.
const GURL page_url(test_server.GetURL(kReportCloseEventsPath));
if (!LoadUrlAndExpectResponse(GetNavigationController(),
fuchsia::web::LoadUrlParams(),
page_url.spec())) {
ADD_FAILURE();
loop_.Quit();
return;
}
navigation_listener().RunUntilUrlEquals(page_url);
// Connect a message port over which to receive notifications of events.
fuchsia::web::WebMessage message;
message.set_data(base::MemBufferFromString(
hang_on_event ? "hang_on_event" : "init", "test"));
message.mutable_outgoing_transfer()->push_back(
std::move(fuchsia::web::OutgoingTransferable().set_message_port(
message_port_.NewRequest())));
base::test::TestFuture<fuchsia::web::Frame_PostMessage_Result> post_result;
get()->PostMessage(test_server.GetOrigin().Serialize(), std::move(message),
CallbackToFitFunction(post_result.GetCallback()));
if (!post_result.Wait()) {
ADD_FAILURE();
loop_.Quit();
return;
}
// If the FrameForTest becomes disconnected then store the epitaph, for
// tests to verify.
ptr().set_error_handler(CallbackToFitFunction(epitaph_.GetCallback()));
// Don't error-out if the Frame disconnects.
navigation_listener_binding().set_error_handler([](zx_status_t) {});
// Start reading messages from the port into a queue, until the port closes.
message_port_.set_error_handler(
[quit = loop_.QuitClosure()](zx_status_t) { std::move(quit).Run(); });
message_port_->ReceiveMessage(
fit::bind_member(this, &FrameForTestWithMessageLog::OnMessage));
}
void RunUntilMessagePortClosed() { loop_.Run(); }
base::test::TestFuture<zx_status_t>& epitaph() { return epitaph_; }
const std::vector<std::string>& events() const { return events_; }
std::string EventsString() const {
return "[" + base::JoinString(events_, ", ") + "]";
}
private:
void OnMessage(fuchsia::web::WebMessage message) {
events_.push_back(std::move(*base::StringFromMemBuffer(message.data())));
message_port_->ReceiveMessage(
fit::bind_member(this, &FrameForTestWithMessageLog::OnMessage));
}
base::RunLoop loop_;
fuchsia::web::MessagePortPtr message_port_;
std::vector<std::string> events_;
base::test::TestFuture<zx_status_t> epitaph_;
};
constexpr char kBeforeUnloadEventName[] = "window.beforeunload";
constexpr char kUnloadEventName[] = "window.unload";
constexpr char kPageHideEventName[] = "window.pagehide";
} // namespace
// Verifies that `Close()`ing a `Frame` without an explicit timeout allows
// graceful teardown, including firing the expected set of events
// ("beforeunload", "pagehide" and "onunload").
IN_PROC_BROWSER_TEST_F(FrameImplTest, Close_EventsWithDefaultTimeout) {
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
FrameForTestWithMessageLog frame(FrameForTest::Create(context(), {}),
*embedded_test_server());
// Request to gracefully close the Frame, which should trigger the
// relevant event handlers, and then quickly tear-down.
frame->Close({});
frame.RunUntilMessagePortClosed();
// Verify that the expected events were delivered!
ASSERT_EQ(frame.events().size(), 3u) << frame.EventsString();
EXPECT_EQ(frame.events()[0], kBeforeUnloadEventName);
EXPECT_EQ(frame.events()[1], kPageHideEventName);
EXPECT_EQ(frame.events()[2], kUnloadEventName);
EXPECT_EQ(frame.epitaph().Get(), ZX_OK);
}
// Verifies that `Close()`ing a `Frame` with an explicit timeout allows
// graceful teardown, dispatching the expected events.
IN_PROC_BROWSER_TEST_F(FrameImplTest, Close_EventsWithNonZeroTimeout) {
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
FrameForTestWithMessageLog frame(FrameForTest::Create(context(), {}),
*embedded_test_server());
// Request to gracefully close the Frame, which should trigger the
// relevant event handlers, and then quickly tear-down.
// Use a timeout long enough that the test would have timed-out if
// teardown had taken so long.
frame->Close(std::move(fuchsia::web::FrameCloseRequest().set_timeout(
TestTimeouts::action_max_timeout().ToZxDuration())));
frame.RunUntilMessagePortClosed();
// Verify that the expected events were delivered!
ASSERT_EQ(frame.events().size(), 3u) << frame.EventsString();
EXPECT_EQ(frame.events()[0], kBeforeUnloadEventName);
EXPECT_EQ(frame.events()[1], kPageHideEventName);
EXPECT_EQ(frame.events()[2], kUnloadEventName);
EXPECT_EQ(frame.epitaph().Get(), ZX_OK);
}
// Verifies that if a `Frame` is `Close()`d with a timeout and takes too long
// to close then `ZX_ERR_TIMED_OUT` is reported.
IN_PROC_BROWSER_TEST_F(FrameImplTest, Close_WithInsufficientTimeout) {
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
FrameForTestWithMessageLog frame(FrameForTest::Create(context(), {}),
*embedded_test_server(),
/* hang_on_event= */ true);
// Request to gracefully close the Frame, by specifying a non-zero timeout.
// This can be arbitrarily short, since we are deliberately provoking timeout.
frame->Close(std::move(fuchsia::web::FrameCloseRequest().set_timeout(
base::Microseconds(1u).ToZxDuration())));
// Don't wait for the MessagePort to close, since that doesn't happen in
// ASAN builds, for some reason (crbug.com/1400304).
// Regardless of how many events may have been delivered, teardown should
// have timed-out.
EXPECT_EQ(frame.epitaph().Get(), ZX_ERR_TIMED_OUT);
}
// Verifies that `Close()`ing a `Frame` with a zero timeout takes immediate
// effect.
IN_PROC_BROWSER_TEST_F(FrameImplTest, Close_NoEventsWithZeroTimeout) {
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
FrameForTestWithMessageLog frame(FrameForTest::Create(context(), {}),
*embedded_test_server());
// Request to close the frame immediately.
frame->Close(std::move(fuchsia::web::FrameCloseRequest().set_timeout(0u)));
frame.RunUntilMessagePortClosed();
// In practice it is possible for content to have time to receive & process
// visibility and unload events, so just check for "beforeunload".
EXPECT_THAT(frame.events(), Not(Contains(kBeforeUnloadEventName)))
<< frame.EventsString();
EXPECT_EQ(frame.epitaph().Get(), ZX_OK);
}
// Verifies that disconnecting a `Frame` takes immediate effect.
IN_PROC_BROWSER_TEST_F(FrameImplTest, Disconnect_NoEvents) {
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle =
embedded_test_server()->StartAndReturnHandle());
FrameForTestWithMessageLog frame(FrameForTest::Create(context(), {}),
*embedded_test_server());
// Request to close the frame immediately.
frame.ptr() = nullptr;
frame.RunUntilMessagePortClosed();
// In practice it is possible for content to have time to receive & process
// visibility and unload events, so just check for "beforeunload".
EXPECT_THAT(frame.events(), Not(Contains(kBeforeUnloadEventName)))
<< frame.EventsString();
}