| // 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/bind.h" |
| #include "base/json/json_reader.h" |
| #include "base/memory/weak_ptr.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_web_contents.h" |
| #include "headless/test/headless_browser_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 { |
| const char kIndexHtml[] = R"( |
| <html> |
| <body> |
| <script src="bindings.js"></script> |
| </body> |
| </html> |
| )"; |
| |
| const char kTabSocketScript[] = R"( |
| window.TabSocket = {}; |
| window.TabSocket.onmessage = () => {}; |
| window.TabSocket.send = (json) => console.debug(json); |
| )"; |
| |
| } // namespace |
| |
| class HeadlessJsBindingsTest |
| : public HeadlessAsyncDevTooledBrowserTest, |
| public HeadlessDevToolsClient::RawProtocolListener, |
| public headless::runtime::Observer, |
| public page::ExperimentalObserver { |
| public: |
| using ConsoleAPICalledParams = headless::runtime::ConsoleAPICalledParams; |
| using EvaluateResult = headless::runtime::EvaluateResult; |
| using RemoteObject = headless::runtime::RemoteObject; |
| |
| HeadlessJsBindingsTest() : weak_factory_(this) {} |
| |
| void SetUpOnMainThread() override { |
| base::ThreadRestrictions::SetIOAllowed(true); |
| base::FilePath pak_path; |
| ASSERT_TRUE(base::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 CustomizeHeadlessWebContents( |
| HeadlessWebContents::Builder& builder) override { |
| builder.SetWindowSize(gfx::Size(0, 0)); |
| builder.SetInitialURL(GURL("http://test.com/index.html")); |
| interceptor_->InsertResponse("http://test.com/index.html", |
| {kIndexHtml, "text/html"}); |
| std::string bindings_js = ui::ResourceBundle::GetSharedInstance() |
| .GetRawDataResource(DEVTOOLS_BINDINGS_TEST) |
| .as_string(); |
| interceptor_->InsertResponse( |
| "http://test.com/bindings.js", |
| {bindings_js.c_str(), "application/javascript"}); |
| } |
| |
| void RunDevTooledTest() override { |
| devtools_client_->GetPage()->GetExperimental()->AddObserver(this); |
| devtools_client_->GetPage()->Enable(); |
| devtools_client_->GetPage()->AddScriptToEvaluateOnNewDocument( |
| kTabSocketScript); |
| devtools_client_->GetRuntime()->AddObserver(this); |
| devtools_client_->GetRuntime()->Enable(); |
| devtools_client_->GetRuntime()->Evaluate( |
| kTabSocketScript, |
| base::BindOnce(&HeadlessJsBindingsTest::ConnectionEstablished, |
| weak_factory_.GetWeakPtr())); |
| devtools_client_->SetRawProtocolListener(this); |
| } |
| |
| void ConnectionEstablished(std::unique_ptr<EvaluateResult>) { |
| connection_established_ = true; |
| } |
| |
| 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 OnConsoleAPICalled(const ConsoleAPICalledParams& params) override { |
| const std::vector<std::unique_ptr<RemoteObject>>& args = *params.GetArgs(); |
| if (args.empty()) |
| return; |
| if (params.GetType() != headless::runtime::ConsoleAPICalledType::DEBUG) |
| return; |
| |
| RemoteObject* object = args[0].get(); |
| if (object->GetType() != headless::runtime::RemoteObjectType::STRING) |
| return; |
| |
| OnMessageFromJS(object->GetValue()->GetString()); |
| } |
| |
| void SendMessageToJS(const std::string& message) { |
| std::string encoded; |
| base::Base64Encode(message, &encoded); |
| devtools_client_->GetRuntime()->Evaluate( |
| "window.TabSocket.onmessage(atob(\"" + encoded + "\"))"); |
| } |
| |
| void OnMessageFromJS(const std::string& json_message) { |
| std::unique_ptr<base::Value> message = |
| base::JSONReader::ReadDeprecated(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& json_message, |
| const base::DictionaryValue& parsed_message) override { |
| if (!connection_established_) |
| 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; |
| |
| SendMessageToJS(json_message); |
| return true; |
| } |
| |
| const base::Value* method_value = parsed_message.FindKey("method"); |
| if (!method_value) |
| return false; |
| |
| if (method_value->GetString() == "Runtime.consoleAPICalled") { |
| // console.debug is used for transport. |
| return false; |
| } |
| |
| SendMessageToJS(json_message); |
| |
| // 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: |
| bool connection_established_ = false; |
| base::WeakPtrFactory<HeadlessJsBindingsTest> weak_factory_; |
| }; |
| |
| 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); |
| |
| } // namespace headless |