blob: cafa883026aeec6dff47e0e262600b3a0ff8475b [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <string>
#include <vector>
#include "base/base64.h"
#include "base/json/json_reader.h"
#include "base/path_service.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/threading/thread_restrictions.h"
#include "content/public/common/isolated_world_ids.h"
#include "content/public/test/browser_test.h"
#include "headless/grit/headless_browsertest_resources.h"
#include "headless/public/devtools/domains/page.h"
#include "headless/public/devtools/domains/runtime.h"
#include "headless/public/headless_browser.h"
#include "headless/public/headless_devtools_client.h"
#include "headless/public/headless_tab_socket.h"
#include "headless/public/headless_web_contents.h"
#include "headless/public/util/testing/test_in_memory_protocol_handler.h"
#include "headless/test/tab_socket_test.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/resource/resource_bundle.h"
namespace headless {
namespace {
static constexpr char kIndexHtml[] = R"(
<html>
<body>
<script src="bindings.js"></script>
</body>
</html>
)";
} // namespace
class HeadlessJsBindingsTest
: public HeadlessAsyncDevTooledBrowserTest,
public HeadlessDevToolsClient::RawProtocolListener,
public TestInMemoryProtocolHandler::RequestDeferrer,
public HeadlessTabSocket::Listener,
public page::ExperimentalObserver {
public:
void SetUp() override {
options()->mojo_service_names.insert("headless::TabSocket");
HeadlessAsyncDevTooledBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
base::ThreadRestrictions::SetIOAllowed(true);
base::FilePath pak_path;
ASSERT_TRUE(PathService::Get(base::DIR_MODULE, &pak_path));
pak_path = pak_path.AppendASCII("headless_browser_tests.pak");
ui::ResourceBundle::GetSharedInstance().AddDataPackFromPath(
pak_path, ui::SCALE_FACTOR_NONE);
}
void CustomizeHeadlessBrowserContext(
HeadlessBrowserContext::Builder& builder) override {
builder.AddTabSocketMojoBindings();
builder.EnableUnsafeNetworkAccessWithMojoBindings(true);
}
void CustomizeHeadlessWebContents(
HeadlessWebContents::Builder& builder) override {
builder.SetWindowSize(gfx::Size(0, 0));
builder.SetInitialURL(GURL("http://test.com/index.html"));
http_handler_->SetHeadlessBrowserContext(browser_context_);
}
bool GetAllowTabSockets() override { return true; }
ProtocolHandlerMap GetProtocolHandlers() override {
ProtocolHandlerMap protocol_handlers;
std::unique_ptr<TestInMemoryProtocolHandler> http_handler(
new TestInMemoryProtocolHandler(browser()->BrowserIOThread(), this));
http_handler_ = http_handler.get();
bindings_js_ = ui::ResourceBundle::GetSharedInstance()
.GetRawDataResource(DEVTOOLS_BINDINGS_TEST)
.as_string();
http_handler->InsertResponse("http://test.com/index.html",
{kIndexHtml, "text/html"});
http_handler->InsertResponse(
"http://test.com/bindings.js",
{bindings_js_.c_str(), "application/javascript"});
protocol_handlers[url::kHttpScheme] = std::move(http_handler);
return protocol_handlers;
}
void RunDevTooledTest() override {
devtools_client_->GetPage()->GetExperimental()->AddObserver(this);
devtools_client_->GetPage()->Enable();
headless_tab_socket_ = web_contents_->GetHeadlessTabSocket();
CHECK(headless_tab_socket_);
headless_tab_socket_->SetListener(this);
devtools_client_->SetRawProtocolListener(this);
headless_tab_socket_->InstallMainFrameMainWorldHeadlessTabSocketBindings(
base::BindOnce(&HeadlessJsBindingsTest::OnInstalledHeadlessTabSocket,
base::Unretained(this)));
}
void OnRequest(const GURL& url,
base::RepeatingClosure complete_request) override {
if (!tab_socket_installed_ && url.spec() == "http://test.com/bindings.js") {
complete_request_ = std::move(complete_request);
} else {
complete_request.Run();
}
}
void OnInstalledHeadlessTabSocket(base::Optional<int> context_id) {
main_world_execution_context_id_ = *context_id;
tab_socket_installed_ = true;
if (complete_request_) {
browser()->BrowserIOThread()->PostTask(FROM_HERE, complete_request_);
complete_request_ = base::RepeatingClosure();
}
}
virtual void RunJsBindingsTest() = 0;
void OnLoadEventFired(const page::LoadEventFiredParams& params) override {
RunJsBindingsTest();
}
virtual void OnResult(const std::string& result) = 0;
void FailOnJsEvaluateException(
std::unique_ptr<runtime::EvaluateResult> result) {
if (!result->HasExceptionDetails())
return;
FinishAsynchronousTest();
const runtime::ExceptionDetails* exception_details =
result->GetExceptionDetails();
FAIL() << exception_details->GetText()
<< (exception_details->HasException()
? exception_details->GetException()->GetDescription().c_str()
: "");
}
void OnMessageFromContext(const std::string& json_message,
int v8_execution_context_id) override {
EXPECT_EQ(main_world_execution_context_id_, v8_execution_context_id);
std::unique_ptr<base::Value> message =
base::JSONReader::Read(json_message, base::JSON_PARSE_RFC);
const base::Value* method_value = message->FindKey("method");
if (!method_value) {
FinishAsynchronousTest();
FAIL() << "Badly formed message " << json_message;
return;
}
const base::Value* params_value = message->FindKey("params");
if (!params_value) {
FinishAsynchronousTest();
FAIL() << "Badly formed message " << json_message;
return;
}
const base::Value* id_value = message->FindKey("id");
if (!id_value) {
FinishAsynchronousTest();
FAIL() << "Badly formed message " << json_message;
return;
}
if (method_value->GetString() == "__Result") {
OnResult(params_value->FindKey("result")->GetString());
return;
}
devtools_client_->SendRawDevToolsMessage(json_message);
}
bool OnProtocolMessage(const std::string& devtools_agent_host_id,
const std::string& json_message,
const base::DictionaryValue& parsed_message) override {
if (main_world_execution_context_id_ == -1)
return false;
const base::Value* id_value = parsed_message.FindKey("id");
// If |parsed_message| contains an id we know this is a message reply.
if (id_value) {
int id = id_value->GetInt();
// We are only interested in message replies (ones with an id) where the
// id is odd. The reason is HeadlessDevToolsClientImpl uses even/oddness
// to distinguish between commands send from the C++ bindings and those
// via HeadlessDevToolsClientImpl::SendRawDevToolsMessage.
if ((id % 2) == 0)
return false;
headless_tab_socket_->SendMessageToContext(
json_message, main_world_execution_context_id_);
return true;
}
const base::Value* method_value = parsed_message.FindKey("method");
if (!method_value)
return false;
headless_tab_socket_->SendMessageToContext(
json_message, main_world_execution_context_id_);
// Check which domain the event belongs to, if it's the DOM domain then
// assume js handled it.
std::vector<base::StringPiece> sections =
SplitStringPiece(method_value->GetString(), ".", base::KEEP_WHITESPACE,
base::SPLIT_WANT_ALL);
return sections[0] == "DOM" || sections[0] == "Runtime";
}
protected:
TestInMemoryProtocolHandler* http_handler_; // NOT OWNED
HeadlessTabSocket* headless_tab_socket_; // NOT OWNED
int main_world_execution_context_id_ = -1;
std::string bindings_js_;
base::RepeatingClosure complete_request_;
bool tab_socket_installed_ = false;
};
class SimpleCommandJsBindingsTest : public HeadlessJsBindingsTest {
public:
void RunJsBindingsTest() override {
devtools_client_->GetRuntime()->Evaluate(
"new chromium.BindingsTest().evalOneAddOne();",
base::BindOnce(&HeadlessJsBindingsTest::FailOnJsEvaluateException,
base::Unretained(this)));
}
void OnResult(const std::string& result) override {
EXPECT_EQ("2", result);
FinishAsynchronousTest();
}
};
HEADLESS_ASYNC_DEVTOOLED_TEST_F(SimpleCommandJsBindingsTest);
class ExperimentalCommandJsBindingsTest : public HeadlessJsBindingsTest {
public:
void RunJsBindingsTest() override {
devtools_client_->GetRuntime()->Evaluate(
"new chromium.BindingsTest().getIsolatedWorldName();",
base::BindOnce(&HeadlessJsBindingsTest::FailOnJsEvaluateException,
base::Unretained(this)));
}
void OnResult(const std::string& result) override {
EXPECT_EQ("Created Test Isolated World", result);
FinishAsynchronousTest();
}
};
HEADLESS_ASYNC_DEVTOOLED_TEST_F(ExperimentalCommandJsBindingsTest);
class SimpleEventJsBindingsTest : public HeadlessJsBindingsTest {
public:
void RunJsBindingsTest() override {
devtools_client_->GetRuntime()->Evaluate(
"new chromium.BindingsTest().listenForChildNodeCountUpdated();",
base::BindOnce(&HeadlessJsBindingsTest::FailOnJsEvaluateException,
base::Unretained(this)));
}
void OnResult(const std::string& result) override {
EXPECT_EQ("{\"nodeId\":4,\"childNodeCount\":2}", result);
FinishAsynchronousTest();
}
};
HEADLESS_ASYNC_DEVTOOLED_TEST_F(SimpleEventJsBindingsTest);
/*
* Like SimpleCommandJsBindingsTest except it's run twice. On the first run
* metadata is produced by v8 for http://test.com/bindings.js. On the second run
* the metadata is used used leading to substantially faster execution time.
*/
class CachedJsBindingsTest : public HeadlessJsBindingsTest,
public HeadlessBrowserContext::Observer {
public:
void CustomizeHeadlessBrowserContext(
HeadlessBrowserContext::Builder& builder) override {
builder.SetCaptureResourceMetadata(true);
builder.SetOverrideWebPreferencesCallback(base::BindRepeating(
&CachedJsBindingsTest::OverrideWebPreferences, base::Unretained(this)));
HeadlessJsBindingsTest::CustomizeHeadlessBrowserContext(builder);
}
void OverrideWebPreferences(WebPreferences* preferences) {
// Request eager code compilation.
preferences->v8_cache_options =
content::V8_CACHE_OPTIONS_FULLCODE_WITHOUT_HEAT_CHECK;
}
void RunDevTooledTest() override {
browser_context_->AddObserver(this);
HeadlessJsBindingsTest::RunDevTooledTest();
}
void RunJsBindingsTest() override {
devtools_client_->GetRuntime()->Evaluate(
"new chromium.BindingsTest().evalOneAddOne();",
base::BindRepeating(&HeadlessJsBindingsTest::FailOnJsEvaluateException,
base::Unretained(this)));
}
void OnResult(const std::string& result) override {
EXPECT_EQ("2", result);
if (first_result) {
tab_socket_installed_ = false;
main_world_execution_context_id_ = -1;
devtools_client_->GetPage()->Reload();
} else {
EXPECT_TRUE(metadata_received_);
FinishAsynchronousTest();
}
first_result = false;
}
// Called on the IO thread.
void OnRequest(const GURL& url,
base::RepeatingClosure complete_request) override {
HeadlessJsBindingsTest::OnRequest(url, complete_request);
if (!first_result && url.spec() == "http://test.com/bindings.js") {
browser()->BrowserMainThread()->PostTask(
FROM_HERE,
base::BindOnce(&CachedJsBindingsTest::ReinstallTabSocketBindings,
base::Unretained(this)));
}
}
void ReinstallTabSocketBindings() {
headless_tab_socket_->InstallMainFrameMainWorldHeadlessTabSocketBindings(
base::BindRepeating(
&HeadlessJsBindingsTest::OnInstalledHeadlessTabSocket,
base::Unretained(this)));
}
void OnMetadataForResource(const GURL& url,
net::IOBuffer* buf,
int buf_len) override {
ASSERT_FALSE(metadata_received_);
metadata_received_ = true;
scoped_refptr<net::IOBufferWithSize> metadata(
new net::IOBufferWithSize(buf_len));
memcpy(metadata->data(), buf->data(), buf_len);
http_handler_->SetResponseMetadata(url.spec(), metadata);
}
bool metadata_received_ = false;
bool first_result = true;
};
HEADLESS_ASYNC_DEVTOOLED_TEST_F(CachedJsBindingsTest);
} // namespace headless