// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROMECAST_BROWSER_CAST_WEB_CONTENTS_BROWSERTEST_H_
#define CHROMECAST_BROWSER_CAST_WEB_CONTENTS_BROWSERTEST_H_
#include <memory>
#include <string>
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/containers/flat_set.h"
#include "base/files/file_util.h"
#include "base/functional/callback_helpers.h"
#include "base/path_service.h"
#include "base/ranges/algorithm.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "chromecast/base/chromecast_switches.h"
#include "chromecast/base/metrics/cast_metrics_helper.h"
#include "chromecast/browser/cast_browser_context.h"
#include "chromecast/browser/cast_browser_process.h"
#include "chromecast/browser/cast_web_contents_impl.h"
#include "chromecast/browser/cast_web_contents_observer.h"
#include "chromecast/browser/mojom/cast_web_service.mojom.h"
#include "chromecast/browser/test_interfaces.test-mojom.h"
#include "chromecast/mojo/interface_bundle.h"
#include "content/public/browser/browser_thread.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/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_base.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/url_loader_interceptor.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "services/service_manager/public/mojom/interface_provider.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
using ::testing::_;
using ::testing::AllOf;
using ::testing::AtLeast;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Expectation;
using ::testing::InSequence;
using ::testing::Invoke;
using ::testing::InvokeWithoutArgs;
using ::testing::Mock;
using ::testing::NiceMock;
using ::testing::Property;
using ::testing::WithArgs;
namespace content {
class WebContents;
}
namespace chromecast {
namespace {
const base::FilePath::CharType kTestDataPath[] =
FILE_PATH_LITERAL("chromecast/browser/test/data");
base::FilePath GetTestDataPath() {
return base::FilePath(kTestDataPath);
}
base::FilePath GetTestDataFilePath(const std::string& name) {
base::FilePath file_path;
CHECK(base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &file_path));
return file_path.Append(GetTestDataPath()).AppendASCII(name);
}
std::unique_ptr<net::test_server::HttpResponse> DefaultHandler(
net::HttpStatusCode status_code,
const net::test_server::HttpRequest& request) {
auto http_response = std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(status_code);
return http_response;
}
// =============================================================================
// Mocks
// =============================================================================
class MockCastWebContentsObserver : public CastWebContentsObserver {
public:
MockCastWebContentsObserver() {}
MockCastWebContentsObserver(const MockCastWebContentsObserver&) = delete;
MockCastWebContentsObserver& operator=(const MockCastWebContentsObserver&) =
delete;
~MockCastWebContentsObserver() override = default;
MOCK_METHOD1(PageStateChanged, void(PageState page_state));
MOCK_METHOD2(PageStopped, void(PageState page_state, int error_code));
MOCK_METHOD2(RenderFrameCreated,
void(int render_process_id, int render_frame_id));
MOCK_METHOD0(ResourceLoadFailed, void());
MOCK_METHOD1(UpdateTitle, void(const std::string& title));
};
class MockWebContentsDelegate : public content::WebContentsDelegate {
public:
MockWebContentsDelegate() = default;
~MockWebContentsDelegate() override = default;
MOCK_METHOD1(CloseContents, void(content::WebContents* source));
};
class TitleChangeObserver : public CastWebContentsObserver {
public:
TitleChangeObserver() = default;
TitleChangeObserver(const TitleChangeObserver&) = delete;
TitleChangeObserver& operator=(const TitleChangeObserver&) = delete;
~TitleChangeObserver() override = default;
// Spins a Runloop until the title of the page matches the |expected_title|
// that have been set.
void RunUntilTitleEquals(const std::string& expected_title) {
expected_title_ = expected_title;
// Spin the runloop until the expected conditions are met.
if (current_title_ != expected_title_) {
expected_title_ = expected_title;
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
run_loop.Run();
}
}
// CastWebContentsObserver implementation:
void UpdateTitle(const std::string& title) override {
// Resumes execution of RunUntilTitleEquals() if |title| matches
// expectations.
current_title_ = title;
if (!quit_closure_.is_null() && current_title_ == expected_title_) {
DCHECK_EQ(current_title_, expected_title_);
std::move(quit_closure_).Run();
}
}
private:
std::string current_title_;
std::string expected_title_;
base::OnceClosure quit_closure_;
};
class TestMessageReceiver : public blink::WebMessagePort::MessageReceiver {
public:
TestMessageReceiver() = default;
TestMessageReceiver(const TestMessageReceiver&) = delete;
TestMessageReceiver& operator=(const TestMessageReceiver&) = delete;
~TestMessageReceiver() override = default;
void WaitForNextIncomingMessage(
base::OnceCallback<void(std::string,
std::optional<blink::WebMessagePort>)> callback) {
DCHECK(message_received_callback_.is_null())
<< "Only one waiting event is allowed.";
message_received_callback_ = std::move(callback);
}
void SetOnPipeErrorCallback(base::OnceCallback<void()> callback) {
on_pipe_error_callback_ = std::move(callback);
}
private:
bool OnMessage(blink::WebMessagePort::Message message) override {
std::string message_text;
if (!base::UTF16ToUTF8(message.data.data(), message.data.size(),
&message_text)) {
return false;
}
std::optional<blink::WebMessagePort> incoming_port = std::nullopt;
// Only one MessagePort should be sent to here.
if (!message.ports.empty()) {
DCHECK(message.ports.size() == 1)
<< "Only one control port can be provided";
incoming_port = std::make_optional<blink::WebMessagePort>(
std::move(message.ports[0]));
}
if (message_received_callback_) {
std::move(message_received_callback_)
.Run(message_text, std::move(incoming_port));
}
return true;
}
void OnPipeError() override {
if (on_pipe_error_callback_)
std::move(on_pipe_error_callback_).Run();
}
base::OnceCallback<void(std::string,
std::optional<blink::WebMessagePort> incoming_port)>
message_received_callback_;
base::OnceCallback<void()> on_pipe_error_callback_;
};
} // namespace
// =============================================================================
// Test class
// =============================================================================
class CastWebContentsBrowserTest : public content::BrowserTestBase,
public content::WebContentsObserver {
public:
CastWebContentsBrowserTest(const CastWebContentsBrowserTest&) = delete;
CastWebContentsBrowserTest& operator=(const CastWebContentsBrowserTest&) =
delete;
protected:
CastWebContentsBrowserTest() = default;
~CastWebContentsBrowserTest() override = default;
void SetUp() final {
SetUpCommandLine(base::CommandLine::ForCurrentProcess());
BrowserTestBase::SetUp();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII(switches::kTestType, "browser");
command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures, "MojoJS");
}
void PreRunTestOnMainThread() override {
// Pump startup related events.
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::RunLoop().RunUntilIdle();
metrics::CastMetricsHelper::GetInstance()->SetDummySessionIdForTesting();
content::WebContents::CreateParams create_params(
shell::CastBrowserProcess::GetInstance()->browser_context(), nullptr);
web_contents_ = content::WebContents::Create(create_params);
web_contents_->SetDelegate(&mock_wc_delegate_);
mojom::CastWebViewParamsPtr params = mojom::CastWebViewParams::New();
params->is_root_window = true;
cast_web_contents_ = std::make_unique<CastWebContentsImpl>(
web_contents_.get(), std::move(params));
mock_cast_wc_observer_.Observe(cast_web_contents_.get());
title_change_observer_.Observe(cast_web_contents_.get());
render_frames_.clear();
content::WebContentsObserver::Observe(web_contents_.get());
run_loop_ = std::make_unique<base::RunLoop>();
}
void PostRunTestOnMainThread() override {
cast_web_contents_.reset();
web_contents_.reset();
}
// content::WebContentsObserver implementation:
void RenderFrameCreated(content::RenderFrameHost* render_frame_host) final {
render_frames_.insert(render_frame_host);
}
void StartTestServer() {
ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
embedded_test_server()->StartAcceptingConnections();
}
void QuitRunLoop() {
DCHECK(run_loop_);
if (run_loop_->running()) {
run_loop_->QuitWhenIdle();
}
}
MockWebContentsDelegate mock_wc_delegate_;
NiceMock<MockCastWebContentsObserver> mock_cast_wc_observer_;
TitleChangeObserver title_change_observer_;
std::unique_ptr<content::WebContents> web_contents_;
std::unique_ptr<CastWebContentsImpl> cast_web_contents_;
std::unique_ptr<base::RunLoop> run_loop_;
base::flat_set<content::RenderFrameHost*> render_frames_;
};
MATCHER_P2(CheckPageState, cwc_ptr, expected_state, "") {
if (arg != cwc_ptr)
return false;
return arg->page_state() == expected_state;
}
// =============================================================================
// Test cases
// =============================================================================
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, Lifecycle) {
// ===========================================================================
// Test: Load a blank page successfully, verify LOADED state.
// ===========================================================================
run_loop_ = std::make_unique<base::RunLoop>();
{
InSequence seq;
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADED))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
}
cast_web_contents_->LoadUrl(GURL(url::kAboutBlankURL));
run_loop_->Run();
// ===========================================================================
// Test: Load a blank page via WebContents API, verify LOADED state.
// ===========================================================================
run_loop_ = std::make_unique<base::RunLoop>();
{
InSequence seq;
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADED))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
}
web_contents_->GetController().LoadURL(GURL(url::kAboutBlankURL),
content::Referrer(),
ui::PAGE_TRANSITION_TYPED, "");
run_loop_->Run();
// ===========================================================================
// Test: Inject an iframe, verify no events are received for the frame.
// ===========================================================================
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(_)).Times(0);
EXPECT_CALL(mock_cast_wc_observer_, PageStopped(_, _)).Times(0);
std::string script =
"var iframe = document.createElement('iframe');"
"document.body.appendChild(iframe);"
"iframe.src = 'about:blank';";
ASSERT_TRUE(ExecJs(web_contents_.get(), script));
// ===========================================================================
// Test: Inject an iframe and navigate it to an error page. Verify no events.
// ===========================================================================
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(_)).Times(0);
EXPECT_CALL(mock_cast_wc_observer_, PageStopped(_, _)).Times(0);
script = "iframe.src = 'https://www.fake-non-existent-cast-page.com';";
ASSERT_TRUE(ExecJs(web_contents_.get(), script));
// ===========================================================================
// Test: Close the CastWebContents. WebContentsDelegate will be told to close
// the page, and then after the timeout elapses CWC will enter the CLOSED
// state and notify that the page has stopped.
// ===========================================================================
run_loop_ = std::make_unique<base::RunLoop>();
EXPECT_CALL(mock_wc_delegate_, CloseContents(web_contents_.get()))
.Times(AtLeast(1));
EXPECT_CALL(mock_cast_wc_observer_, PageStopped(PageState::CLOSED, net::OK))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
cast_web_contents_->ClosePage();
run_loop_->Run();
// ===========================================================================
// Test: Destroy the underlying WebContents. Verify DESTROYED state.
// ===========================================================================
run_loop_ = std::make_unique<base::RunLoop>();
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::DESTROYED))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
web_contents_.reset();
run_loop_->Run();
cast_web_contents_.reset();
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, WebContentsDestroyed) {
run_loop_ = std::make_unique<base::RunLoop>();
{
InSequence seq;
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADED))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
}
cast_web_contents_->LoadUrl(GURL(url::kAboutBlankURL));
run_loop_->Run();
// ===========================================================================
// Test: Destroy the WebContents. Verify PageStopped(DESTROYED, net::OK).
// ===========================================================================
run_loop_ = std::make_unique<base::RunLoop>();
EXPECT_CALL(mock_cast_wc_observer_,
PageStopped(PageState::DESTROYED, net::OK))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
web_contents_.reset();
run_loop_->Run();
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, ErrorPageCrash) {
// ===========================================================================
// Test: If the page's main render process crashes, enter ERROR state.
// ===========================================================================
run_loop_ = std::make_unique<base::RunLoop>();
{
InSequence seq;
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADED))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
}
cast_web_contents_->LoadUrl(GURL(url::kAboutBlankURL));
run_loop_->Run();
run_loop_ = std::make_unique<base::RunLoop>();
EXPECT_CALL(mock_cast_wc_observer_,
PageStopped(PageState::ERROR, net::ERR_UNEXPECTED))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
CrashTab(web_contents_.get());
run_loop_->Run();
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, ErrorLocalFileMissing) {
// ===========================================================================
// Test: Loading a page with an HTTP error should enter ERROR state.
// ===========================================================================
run_loop_ = std::make_unique<base::RunLoop>();
{
InSequence seq;
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(mock_cast_wc_observer_, PageStopped(PageState::ERROR, _))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
}
base::FilePath path = GetTestDataFilePath("this_file_does_not_exist.html");
cast_web_contents_->LoadUrl(content::GetFileUrlWithQuery(path, ""));
run_loop_->Run();
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, ErrorLoadFailSubFrames) {
// ===========================================================================
// Test: Ignore load errors in sub-frames.
// ===========================================================================
run_loop_ = std::make_unique<base::RunLoop>();
{
InSequence seq;
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADED))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
}
cast_web_contents_->LoadUrl(GURL(url::kAboutBlankURL));
run_loop_->Run();
// Create a sub-frame.
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(_)).Times(0);
EXPECT_CALL(mock_cast_wc_observer_, PageStopped(_, _)).Times(0);
std::string script =
"var iframe = document.createElement('iframe');"
"document.body.appendChild(iframe);"
"iframe.src = 'about:blank';";
ASSERT_TRUE(ExecJs(web_contents_.get(), script));
ASSERT_EQ(2, (int)render_frames_.size());
auto it =
base::ranges::find(render_frames_, web_contents_->GetPrimaryMainFrame(),
&content::RenderFrameHost::GetParent);
ASSERT_NE(render_frames_.end(), it);
content::RenderFrameHost* sub_frame = *it;
ASSERT_NE(nullptr, sub_frame);
cast_web_contents_->DidFailLoad(sub_frame, sub_frame->GetLastCommittedURL(),
net::ERR_FAILED);
// ===========================================================================
// Test: Ignore main frame load failures with net::ERR_ABORTED.
// ===========================================================================
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(_)).Times(0);
EXPECT_CALL(mock_cast_wc_observer_, PageStopped(_, _)).Times(0);
cast_web_contents_->DidFailLoad(
web_contents_->GetPrimaryMainFrame(),
web_contents_->GetPrimaryMainFrame()->GetLastCommittedURL(),
net::ERR_ABORTED);
// ===========================================================================
// Test: If main frame fails to load, page should enter ERROR state.
// ===========================================================================
run_loop_ = std::make_unique<base::RunLoop>();
EXPECT_CALL(mock_cast_wc_observer_,
PageStopped(PageState::ERROR, net::ERR_FAILED))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
cast_web_contents_->DidFailLoad(
web_contents_->GetPrimaryMainFrame(),
web_contents_->GetPrimaryMainFrame()->GetLastCommittedURL(),
net::ERR_FAILED);
run_loop_->Run();
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, ErrorHttp4XX) {
// ===========================================================================
// Test: If a server responds with an HTTP 4XX error, page should enter ERROR
// state.
// ===========================================================================
embedded_test_server()->RegisterRequestHandler(
base::BindRepeating(&DefaultHandler, net::HTTP_NOT_FOUND));
StartTestServer();
run_loop_ = std::make_unique<base::RunLoop>();
{
InSequence seq;
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(
mock_cast_wc_observer_,
PageStopped(PageState::ERROR, net::ERR_HTTP_RESPONSE_CODE_FAILURE))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
}
cast_web_contents_->LoadUrl(embedded_test_server()->GetURL("/dummy.html"));
run_loop_->Run();
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, ErrorLoadFailed) {
// ===========================================================================
// Test: When main frame load fails, enter ERROR state. This test simulates a
// load error by intercepting the URL request and failing it with an arbitrary
// error code.
// ===========================================================================
base::FilePath path = GetTestDataFilePath("dummy.html");
GURL gurl = content::GetFileUrlWithQuery(path, "");
content::URLLoaderInterceptor url_interceptor(base::BindRepeating(
[](const GURL& url,
content::URLLoaderInterceptor::RequestParams* params) {
if (params->url_request.url != url)
return false;
network::URLLoaderCompletionStatus status;
status.error_code = net::ERR_ADDRESS_UNREACHABLE;
params->client->OnComplete(status);
return true;
},
gurl));
run_loop_ = std::make_unique<base::RunLoop>();
{
InSequence seq;
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(mock_cast_wc_observer_,
PageStopped(PageState::ERROR, net::ERR_ADDRESS_UNREACHABLE))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
}
cast_web_contents_->LoadUrl(gurl);
run_loop_->Run();
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, LoadCanceledByApp) {
// ===========================================================================
// Test: When the app calls window.stop(), the page should not enter the ERROR
// state. Instead, we treat it as LOADED. This is a historical behavior for
// some apps which intentionally stop the page and reload content.
// ===========================================================================
embedded_test_server()->ServeFilesFromSourceDirectory(GetTestDataPath());
StartTestServer();
run_loop_ = std::make_unique<base::RunLoop>();
{
InSequence seq;
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADED))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
}
cast_web_contents_->LoadUrl(
embedded_test_server()->GetURL("/load_cancel.html"));
run_loop_->Run();
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, LocationRedirectLifecycle) {
// ===========================================================================
// Test: When the app redirects to another url via window.location. Another
// navigation will be committed. LOADING -> LOADED -> LOADING -> LOADED state
// trasition is expected.
// ===========================================================================
embedded_test_server()->ServeFilesFromSourceDirectory(GetTestDataPath());
StartTestServer();
run_loop_ = std::make_unique<base::RunLoop>();
{
InSequence seq;
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADED));
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADED))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
}
cast_web_contents_->LoadUrl(
embedded_test_server()->GetURL("/location_redirect.html"));
run_loop_->Run();
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, NotifyMissingResource) {
// ===========================================================================
// Test: Loading a page with a missing resource should notify observers.
// ===========================================================================
run_loop_ = std::make_unique<base::RunLoop>();
{
InSequence seq;
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADED))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
}
EXPECT_CALL(mock_cast_wc_observer_, ResourceLoadFailed());
base::FilePath path = GetTestDataFilePath("missing_resource.html");
cast_web_contents_->LoadUrl(content::GetFileUrlWithQuery(path, ""));
run_loop_->Run();
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, ExecuteJavaScriptOnLoad) {
// ===========================================================================
// Test: Injecting script to change title should work.
// ===========================================================================
const std::string kExpectedTitle = "hello";
const std::string kOriginalTitle =
"Welcome to Stan the Offline Dino's Homepage";
// The script should be able to run before HTML <script> tag starts running.
// The original title will be loaded first and then the injected script. Other
// scripts must run after the injected script.
EXPECT_CALL(mock_cast_wc_observer_, UpdateTitle(kExpectedTitle));
EXPECT_CALL(mock_cast_wc_observer_, UpdateTitle(kOriginalTitle));
constexpr uint64_t kBindingsId = 1234;
GURL gurl = content::GetFileUrlWithQuery(
GetTestDataFilePath("dynamic_title.html"), "");
cast_web_contents_->AddBeforeLoadJavaScript(kBindingsId,
"stashed_title = 'hello';");
cast_web_contents_->LoadUrl(gurl);
title_change_observer_.RunUntilTitleEquals(kExpectedTitle);
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest,
ExecuteJavaScriptUpdatedOnLoad) {
// ===========================================================================
// Test: Verify that this script replaces the previous script with same
// binding id, as opposed to being injected alongside it. (The latter would
// result in the title being "helloclobber").
// ===========================================================================
const std::string kReplaceTitle = "clobber";
const std::string kOriginalTitle =
"Welcome to Stan the Offline Dino's Homepage";
// The script should be able to run before HTML <script> tag starts running.
EXPECT_CALL(mock_cast_wc_observer_, UpdateTitle(kReplaceTitle));
EXPECT_CALL(mock_cast_wc_observer_, UpdateTitle(kOriginalTitle));
constexpr uint64_t kBindingsId = 1234;
GURL gurl = content::GetFileUrlWithQuery(
GetTestDataFilePath("dynamic_title.html"), "");
cast_web_contents_->AddBeforeLoadJavaScript(kBindingsId,
"stashed_title = 'hello';");
cast_web_contents_->AddBeforeLoadJavaScript(
kBindingsId, "stashed_title = document.title + 'clobber';");
cast_web_contents_->LoadUrl(gurl);
title_change_observer_.RunUntilTitleEquals(kReplaceTitle);
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest,
ExecuteJavaScriptOnLoadOrdered) {
// ===========================================================================
// Test: Verifies that bindings are injected in order by producing a
// cumulative, non-commutative result.
// ===========================================================================
const std::string kExpectedTitle = "hello there";
const std::string kOriginalTitle =
"Welcome to Stan the Offline Dino's Homepage";
constexpr int64_t kBindingsId1 = 1234;
constexpr int64_t kBindingsId2 = 5678;
// The script should be able to run before HTML <script> tag starts running.
// The original title will be loaded first and then the injected script. Other
// scripts must run after the injected script.
EXPECT_CALL(mock_cast_wc_observer_, UpdateTitle(kExpectedTitle));
EXPECT_CALL(mock_cast_wc_observer_, UpdateTitle(kOriginalTitle));
GURL gurl = content::GetFileUrlWithQuery(
GetTestDataFilePath("dynamic_title.html"), "");
cast_web_contents_->AddBeforeLoadJavaScript(kBindingsId1,
"stashed_title = 'hello';");
cast_web_contents_->AddBeforeLoadJavaScript(kBindingsId2,
"stashed_title += ' there';");
cast_web_contents_->LoadUrl(gurl);
title_change_observer_.RunUntilTitleEquals(kExpectedTitle);
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest,
ExecuteJavaScriptOnLoadEarlyAndLateRegistrations) {
// ===========================================================================
// Test: Tests that we can inject scripts before and after RenderFrame
// creation.
// ===========================================================================
const std::string kExpectedTitle1 = "foo";
const std::string kExpectedTitle2 = "foo bar";
const std::string kOriginalTitle =
"Welcome to Stan the Offline Dino's Homepage";
constexpr int64_t kBindingsId1 = 1234;
constexpr int64_t kBindingsId2 = 5678;
// The script should be able to run before HTML <script> tag starts running.
// The original title will be loaded first and then the injected script. Other
// scripts must run after the injected script.
EXPECT_CALL(mock_cast_wc_observer_, UpdateTitle(kExpectedTitle2));
EXPECT_CALL(mock_cast_wc_observer_, UpdateTitle(kExpectedTitle1));
EXPECT_CALL(mock_cast_wc_observer_, UpdateTitle(kOriginalTitle)).Times(2);
GURL gurl = content::GetFileUrlWithQuery(
GetTestDataFilePath("dynamic_title.html"), "");
cast_web_contents_->AddBeforeLoadJavaScript(kBindingsId1,
"stashed_title = 'foo';");
cast_web_contents_->LoadUrl(gurl);
title_change_observer_.RunUntilTitleEquals(kExpectedTitle1);
// Inject bindings after RenderFrameCreation
cast_web_contents_->AddBeforeLoadJavaScript(kBindingsId2,
"stashed_title += ' bar';");
// Navigate away to clean the state.
cast_web_contents_->LoadUrl(GURL(url::kAboutBlankURL));
// Navigate back and see if both scripts are working.
cast_web_contents_->LoadUrl(gurl);
title_change_observer_.RunUntilTitleEquals(kExpectedTitle2);
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, PostMessageToMainFrame) {
// ===========================================================================
// Test: Tests that we can trigger onmessage event on a web page. This test
// would post a message to the test page to redirect it to |title1.html|.
// ===========================================================================
constexpr char kOriginalTitle[] = "postmessage";
constexpr char kPage1Path[] = "title1.html";
constexpr char kPage1Title[] = "title 1";
EXPECT_CALL(mock_cast_wc_observer_, UpdateTitle(kPage1Title));
EXPECT_CALL(mock_cast_wc_observer_, UpdateTitle(kOriginalTitle));
embedded_test_server()->ServeFilesFromSourceDirectory(GetTestDataPath());
StartTestServer();
GURL gurl = embedded_test_server()->GetURL("/window_post_message.html");
cast_web_contents_->LoadUrl(gurl);
title_change_observer_.RunUntilTitleEquals(kOriginalTitle);
cast_web_contents_->PostMessageToMainFrame(
gurl.DeprecatedGetOriginAsURL().spec(), kPage1Path,
std::vector<blink::WebMessagePort>());
title_change_observer_.RunUntilTitleEquals(kPage1Title);
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, PostMessagePassMessagePort) {
// ===========================================================================
// Test: Send a MessagePort to the page, then perform bidirectional messaging
// through the port.
// ===========================================================================
constexpr char kOriginalTitle[] = "messageport";
constexpr char kHelloMsg[] = "hi";
constexpr char16_t kPingMsg[] = u"ping";
EXPECT_CALL(mock_cast_wc_observer_, UpdateTitle(kOriginalTitle));
// Load test page.
embedded_test_server()->ServeFilesFromSourceDirectory(GetTestDataPath());
StartTestServer();
GURL gurl = embedded_test_server()->GetURL("/message_port.html");
cast_web_contents_->LoadUrl(gurl);
title_change_observer_.RunUntilTitleEquals(kOriginalTitle);
auto message_pipe = blink::WebMessagePort::CreatePair();
auto platform_port = std::move(message_pipe.first);
auto page_port = std::move(message_pipe.second);
TestMessageReceiver message_receiver;
platform_port.SetReceiver(&message_receiver,
base::SingleThreadTaskRunner::GetCurrentDefault());
// Make sure we could send a MessagePort (ScopedMessagePipeHandle) to the
// page.
{
base::RunLoop run_loop;
auto quit_closure = run_loop.QuitClosure();
auto received_message_callback = base::BindOnce(
[](base::OnceClosure loop_quit_closure, std::string port_msg,
std::optional<blink::WebMessagePort> incoming_port) {
EXPECT_EQ("got_port", port_msg);
std::move(loop_quit_closure).Run();
},
std::move(quit_closure));
message_receiver.WaitForNextIncomingMessage(
std::move(received_message_callback));
std::vector<blink::WebMessagePort> message_ports;
message_ports.push_back(std::move(page_port));
cast_web_contents_->PostMessageToMainFrame(
gurl.DeprecatedGetOriginAsURL().spec(), kHelloMsg,
std::move(message_ports));
run_loop.Run();
}
// Test whether we could receive the right response from the page after we
// send messages through |platform_port|.
{
base::RunLoop run_loop;
auto quit_closure = run_loop.QuitClosure();
auto received_message_callback = base::BindOnce(
[](base::OnceClosure loop_quit_closure, std::string port_msg,
std::optional<blink::WebMessagePort> incoming_port) {
EXPECT_EQ("ack ping", port_msg);
std::move(loop_quit_closure).Run();
},
std::move(quit_closure));
message_receiver.WaitForNextIncomingMessage(
std::move(received_message_callback));
platform_port.PostMessage(blink::WebMessagePort::Message(kPingMsg));
run_loop.Run();
}
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest,
PostMessageMessagePortDisconnected) {
// ===========================================================================
// Test: Send a MessagePort to the page, then perform bidirectional messaging
// through the port. Make sure mojo counterpart pipe handle could receive the
// MessagePort disconnection event.
// ===========================================================================
constexpr char kOriginalTitle[] = "messageport";
constexpr char kHelloMsg[] = "hi";
EXPECT_CALL(mock_cast_wc_observer_, UpdateTitle(kOriginalTitle));
// Load test page.
embedded_test_server()->ServeFilesFromSourceDirectory(GetTestDataPath());
StartTestServer();
GURL gurl = embedded_test_server()->GetURL("/message_port.html");
cast_web_contents_->LoadUrl(gurl);
title_change_observer_.RunUntilTitleEquals(kOriginalTitle);
auto message_pipe = blink::WebMessagePort::CreatePair();
auto platform_port = std::move(message_pipe.first);
auto page_port = std::move(message_pipe.second);
// Bind platform side port
TestMessageReceiver message_receiver;
platform_port.SetReceiver(&message_receiver,
base::SingleThreadTaskRunner::GetCurrentDefault());
// Make sure we could post a MessagePort (ScopedMessagePipeHandle) to
// the page.
{
base::RunLoop run_loop;
auto quit_closure = run_loop.QuitClosure();
auto received_message_callback = base::BindOnce(
[](base::OnceClosure loop_quit_closure, std::string port_msg,
std::optional<blink::WebMessagePort> incoming_port) {
EXPECT_EQ("got_port", port_msg);
std::move(loop_quit_closure).Run();
},
std::move(quit_closure));
message_receiver.WaitForNextIncomingMessage(
std::move(received_message_callback));
std::vector<blink::WebMessagePort> message_ports;
message_ports.push_back(std::move(page_port));
cast_web_contents_->PostMessageToMainFrame(
gurl.DeprecatedGetOriginAsURL().spec(), kHelloMsg,
std::move(message_ports));
run_loop.Run();
}
// Navigating off-page should tear down the MessageChannel, native side
// should be able to receive disconnected event.
{
base::RunLoop run_loop;
message_receiver.SetOnPipeErrorCallback(base::BindOnce(
[](base::OnceClosure quit_closure) { std::move(quit_closure).Run(); },
run_loop.QuitClosure()));
cast_web_contents_->LoadUrl(GURL(url::kAboutBlankURL));
run_loop.Run();
}
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, ExecuteJavaScript) {
// Start test server for hosting test HTML pages.
embedded_test_server()->ServeFilesFromSourceDirectory(GetTestDataPath());
StartTestServer();
// ===========================================================================
// Test: Set a value using ExecuteJavaScript with empty callback, and then use
// ExecuteJavaScript with callback to retrieve that value.
// ===========================================================================
constexpr char kSoyMilkJsonStringLiteral[] = "\"SoyMilk\"";
constexpr char16_t kSoyMilkJsonStringLiteral16[] = u"\"SoyMilk\"";
// Load page with title "hello":
GURL gurl{embedded_test_server()->GetURL("/title1.html")};
run_loop_ = std::make_unique<base::RunLoop>();
{
InSequence seq;
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADED))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
}
cast_web_contents_->LoadUrl(gurl);
run_loop_->Run();
// Execute with empty callback.
cast_web_contents_->ExecuteJavaScript(
base::StrCat({u"const the_var = ", kSoyMilkJsonStringLiteral16, u";"}),
base::DoNothing());
// Execute a script snippet to return the variable's value.
base::RunLoop run_loop2;
cast_web_contents_->ExecuteJavaScript(
u"the_var;", base::BindLambdaForTesting([&](base::Value result_value) {
std::string result_json;
ASSERT_TRUE(base::JSONWriter::Write(result_value, &result_json));
EXPECT_EQ(result_json, kSoyMilkJsonStringLiteral);
run_loop2.Quit();
}));
run_loop2.Run();
}
// Mock class used by the following test case.
class MockApiBindings : public mojom::ApiBindings {
public:
MockApiBindings() = default;
~MockApiBindings() override = default;
mojo::PendingRemote<mojom::ApiBindings> CreateRemote() {
DCHECK(!receiver_.is_bound());
mojo::PendingRemote<mojom::ApiBindings> pending_remote =
receiver_.BindNewPipeAndPassRemote();
return pending_remote;
}
// mojom::ApiBindings implementation:
MOCK_METHOD(void, GetAll, (GetAllCallback), (override));
MOCK_METHOD(void,
Connect,
(const std::string&, blink::MessagePortDescriptor),
(override));
private:
mojo::Receiver<mojom::ApiBindings> receiver_{this};
};
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest,
InjectBindingsFromApiBindingsRemote) {
// Start test server for hosting test HTML pages.
embedded_test_server()->ServeFilesFromSourceDirectory(GetTestDataPath());
StartTestServer();
// ===========================================================================
// Test: Inject a set of scripts to eval an result. Retrieve that value and
// match against the right answer.
// ===========================================================================
MockApiBindings mock_api_bindings;
EXPECT_CALL(mock_api_bindings, GetAll(_))
.Times(1)
.WillOnce(
WithArgs<0>(Invoke([](MockApiBindings::GetAllCallback callback) {
std::vector<chromecast::mojom::ApiBindingPtr> bindings_vector;
bindings_vector.emplace_back(
chromecast::mojom::ApiBinding::New("let res = 0;"));
bindings_vector.emplace_back(
chromecast::mojom::ApiBinding::New("res += 1;"));
bindings_vector.emplace_back(
chromecast::mojom::ApiBinding::New("res += 2;"));
bindings_vector.emplace_back(
chromecast::mojom::ApiBinding::New("res += 3;"));
std::move(callback).Run(std::move(bindings_vector));
})));
// Binds mocked |mojom::ApiBindings|.
cast_web_contents_->ConnectToBindingsService(
mock_api_bindings.CreateRemote());
run_loop_ = std::make_unique<base::RunLoop>();
{
InSequence seq;
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADED))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
}
// Loads a blank page.
cast_web_contents_->LoadUrl(GURL(url::kAboutBlankURL));
run_loop_->Run();
// Evaluates the value of |res|.
EXPECT_EQ(6, content::EvalJs(cast_web_contents_->web_contents(), "res;"));
}
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest,
StopPageInCaseOfEmptyBindingsReceived) {
// Start test server for hosting test HTML pages.
embedded_test_server()->ServeFilesFromSourceDirectory(GetTestDataPath());
StartTestServer();
// ===========================================================================
// Test: Sending empty set of bindings should result in error page state.
// ===========================================================================
MockApiBindings mock_api_bindings;
EXPECT_CALL(mock_api_bindings, GetAll(_))
.Times(1)
.WillOnce(
WithArgs<0>(Invoke([](MockApiBindings::GetAllCallback callback) {
std::vector<chromecast::mojom::ApiBindingPtr> bindings_vector;
std::move(callback).Run(std::move(bindings_vector));
})));
// Binds mocked |mojom::ApiBindings|.
cast_web_contents_->ConnectToBindingsService(
mock_api_bindings.CreateRemote());
run_loop_ = std::make_unique<base::RunLoop>();
{
InSequence seq;
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADING));
EXPECT_CALL(mock_cast_wc_observer_, PageStateChanged(PageState::LOADED))
.WillOnce(InvokeWithoutArgs([&]() { QuitRunLoop(); }));
}
EXPECT_CALL(mock_cast_wc_observer_,
PageStopped(PageState::ERROR, net::ERR_UNEXPECTED));
// Loads a blank page.
cast_web_contents_->LoadUrl(GURL(url::kAboutBlankURL));
run_loop_->Run();
}
// Helper for the test below. This exposes two interfaces, TestAdder and
// TestDoubler.
class TestInterfaceProvider : public mojom::TestAdder,
public mojom::TestDoubler {
public:
TestInterfaceProvider() = default;
~TestInterfaceProvider() override = default;
size_t num_adders() const { return adders_.size(); }
size_t num_doublers() const { return doublers_.size(); }
base::RepeatingCallback<void(mojo::PendingReceiver<mojom::TestAdder>)>
MakeAdderBinder() {
return base::BindLambdaForTesting(
[this](mojo::PendingReceiver<mojom::TestAdder> receiver) {
adders_.Add(this, std::move(receiver));
OnRequestHandled();
});
}
base::RepeatingCallback<void(mojo::PendingReceiver<mojom::TestDoubler>)>
MakeDoublerBinder() {
return base::BindLambdaForTesting(
[this](mojo::PendingReceiver<mojom::TestDoubler> receiver) {
doublers_.Add(this, std::move(receiver));
OnRequestHandled();
});
}
// Waits for some number of new interface binding requests to be dispatched
// and then invokes `callback`.
void WaitForRequests(size_t n, base::OnceClosure callback) {
wait_callback_ = std::move(callback);
num_requests_to_wait_for_ = n;
}
// mojom::TestAdder:
void Add(int32_t a, int32_t b, AddCallback callback) override {
std::move(callback).Run(a + b);
}
// mojom::TestDouble:
void Double(int32_t x, DoubleCallback callback) override {
std::move(callback).Run(x * 2);
}
private:
void OnRequestHandled() {
if (num_requests_to_wait_for_ == 0)
return;
DCHECK(wait_callback_);
if (--num_requests_to_wait_for_ == 0)
std::move(wait_callback_).Run();
}
mojo::ReceiverSet<mojom::TestAdder> adders_;
mojo::ReceiverSet<mojom::TestDoubler> doublers_;
size_t num_requests_to_wait_for_ = 0;
base::OnceClosure wait_callback_;
};
IN_PROC_BROWSER_TEST_F(CastWebContentsBrowserTest, InterfaceBinding) {
// This test verifies that interfaces registered with the CastWebContents --
// either via its binder_registry() or its RegisterInterfaceProvider() API --
// are reachable from render frames using either the deprecated
// InterfaceProvider API (which results in an OnInterfaceRequestFromFrame call
// on the WebContents) or the newer BrowserInterfaceBroker API which is used
// in most other places (including from Mojo JS).
TestInterfaceProvider provider;
InterfaceBundle bundle_;
bundle_.AddBinder(provider.MakeAdderBinder());
bundle_.AddBinder(provider.MakeDoublerBinder());
cast_web_contents_->SetInterfacesForRenderer(bundle_.CreateRemote());
// First verify that both interfaces are reachable using the deprecated
// WebContents path, which is triggered only by renderer-side use of
// RenderFrame::GetRemoteInterfaces(). Since poking renderer state in browser
// tests is challenging, we simply simulate the resulting WebContentsObbserver
// calls here instead and verify end-to-end connection for each interface.
mojo::Remote<mojom::TestAdder> adder;
mojo::GenericPendingReceiver adder_receiver(
adder.BindNewPipeAndPassReceiver());
EXPECT_TRUE(cast_web_contents_->TryBindReceiver(adder_receiver));
mojo::Remote<mojom::TestDoubler> doubler;
mojo::GenericPendingReceiver doubler_receiver(
doubler.BindNewPipeAndPassReceiver());
EXPECT_TRUE(cast_web_contents_->TryBindReceiver(doubler_receiver));
base::RunLoop add_loop;
adder->Add(37, 5, base::BindLambdaForTesting([&](int32_t result) {
EXPECT_EQ(42, result);
add_loop.Quit();
}));
add_loop.Run();
base::RunLoop double_loop;
doubler->Double(21, base::BindLambdaForTesting([&](int32_t result) {
EXPECT_EQ(42, result);
double_loop.Quit();
}));
double_loop.Run();
EXPECT_EQ(1u, provider.num_adders());
EXPECT_EQ(1u, provider.num_doublers());
// Now verify that the same interfaces are also reachable at the same binders
// when going through the newer BrowserInterfaceBroker path. For simplicity
// the test JS here does not have access to bindings and so does not make
// calls on the interfaces. It is however totally sufficient for us to verify
// that the page's requests result in new receivers being bound inside
// TestInterfaceProvider.
base::RunLoop loop;
provider.WaitForRequests(2, loop.QuitClosure());
embedded_test_server()->ServeFilesFromSourceDirectory(GetTestDataPath());
StartTestServer();
const GURL kUrl{embedded_test_server()->GetURL("/interface_binding.html")};
cast_web_contents_->LoadUrl(kUrl);
loop.Run();
EXPECT_EQ(2u, provider.num_adders());
EXPECT_EQ(2u, provider.num_doublers());
}
} // namespace chromecast
#endif // CHROMECAST_BROWSER_CAST_WEB_CONTENTS_BROWSERTEST_H_