| // Copyright (c) 2020 The Chromium Embedded Framework 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 <algorithm> |
| #include <sstream> |
| |
| #include "include/base/cef_bind.h" |
| #include "include/cef_callback.h" |
| #include "include/cef_devtools_message_observer.h" |
| #include "include/cef_parser.h" |
| #include "include/wrapper/cef_closure_task.h" |
| |
| #include "tests/ceftests/test_handler.h" |
| #include "tests/gtest/include/gtest/gtest.h" |
| |
| namespace { |
| |
| const char kTestUrl1[] = "http://tests/DevToolsMessage1"; |
| const char kTestUrl2[] = "http://tests/DevToolsMessage2"; |
| |
| class DevToolsMessageTestHandler : public TestHandler { |
| public: |
| DevToolsMessageTestHandler() {} |
| |
| void RunTest() override { |
| // Add HTML resources. |
| AddResource(kTestUrl1, "<html><body>Test1</body></html>", "text/html"); |
| AddResource(kTestUrl2, "<html><body>Test2</body></html>", "text/html"); |
| |
| // Create the browser. |
| CreateBrowser(kTestUrl1); |
| |
| // Time out the test after a reasonable period of time. |
| SetTestTimeout(); |
| } |
| |
| void OnAfterCreated(CefRefPtr<CefBrowser> browser) override { |
| TestHandler::OnAfterCreated(browser); |
| |
| // STEP 1: Add the DevTools observer. Wait for the 1st load. |
| registration_ = browser->GetHost()->AddDevToolsMessageObserver( |
| new TestMessageObserver(this)); |
| EXPECT_TRUE(registration_); |
| } |
| |
| void OnLoadingStateChange(CefRefPtr<CefBrowser> browser, |
| bool isLoading, |
| bool canGoBack, |
| bool canGoForward) override { |
| if (!isLoading) { |
| load_ct_++; |
| if (load_ct_ == 1) { |
| // STEP 2: 1st load has completed. Now enable page domain notifications |
| // and wait for the method result. |
| ExecuteMethod("Page.enable", "", |
| base::Bind(&DevToolsMessageTestHandler::Navigate, this)); |
| } else if (load_ct_ == 2) { |
| MaybeDestroyTest(); |
| } |
| } |
| } |
| |
| void DestroyTest() override { |
| // STEP 7: Remove the DevTools observer. This should result in the observer |
| // object being destroyed. |
| EXPECT_TRUE(registration_); |
| registration_ = nullptr; |
| EXPECT_TRUE(observer_destroyed_); |
| |
| // Each send message variant should be called at least a single time. |
| EXPECT_GE(method_send_ct_, 1); |
| EXPECT_GE(method_execute_ct_, 1); |
| |
| // All sent messages should receive a result callback. |
| EXPECT_EQ(expected_method_ct_, method_send_ct_ + method_execute_ct_); |
| EXPECT_EQ(expected_method_ct_, result_ct_); |
| EXPECT_EQ(expected_method_ct_, last_result_id_); |
| |
| // Every received message should parse successfully to a result or event |
| // callback. |
| EXPECT_EQ(message_ct_, result_ct_ + event_ct_); |
| |
| // Should receive 1 or more events (probably just 1, but who knows?). |
| EXPECT_GE(event_ct_, 1); |
| |
| // OnLoadingStateChange(isLoading=false) should be called twice. |
| EXPECT_EQ(expected_load_ct_, load_ct_); |
| |
| // Should get callbacks for agent attached but not detached. |
| EXPECT_EQ(1, attached_ct_); |
| EXPECT_EQ(0, detached_ct_); |
| |
| TestHandler::DestroyTest(); |
| } |
| |
| private: |
| struct MethodResult { |
| int message_id; |
| bool success; |
| std::string result; |
| }; |
| |
| struct Event { |
| std::string method; |
| std::string params; |
| }; |
| |
| class TestMessageObserver : public CefDevToolsMessageObserver { |
| public: |
| explicit TestMessageObserver(DevToolsMessageTestHandler* handler) |
| : handler_(handler) {} |
| |
| virtual ~TestMessageObserver() { handler_->observer_destroyed_.yes(); } |
| |
| bool OnDevToolsMessage(CefRefPtr<CefBrowser> browser, |
| const void* message, |
| size_t message_size) override { |
| EXPECT_TRUE(browser.get()); |
| EXPECT_EQ(handler_->GetBrowserId(), browser->GetIdentifier()); |
| handler_->message_ct_++; |
| return false; |
| } |
| |
| void OnDevToolsMethodResult(CefRefPtr<CefBrowser> browser, |
| int message_id, |
| bool success, |
| const void* result, |
| size_t result_size) override { |
| EXPECT_TRUE(browser.get()); |
| EXPECT_EQ(handler_->GetBrowserId(), browser->GetIdentifier()); |
| handler_->result_ct_++; |
| |
| MethodResult mr; |
| mr.message_id = message_id; |
| mr.success = success; |
| if (result) { |
| // Intentionally truncating at small size. |
| mr.result = std::string(static_cast<const char*>(result), |
| std::min(static_cast<int>(result_size), 80)); |
| } |
| handler_->OnMethodResult(mr); |
| } |
| |
| void OnDevToolsEvent(CefRefPtr<CefBrowser> browser, |
| const CefString& method, |
| const void* params, |
| size_t params_size) override { |
| EXPECT_TRUE(browser.get()); |
| EXPECT_EQ(handler_->GetBrowserId(), browser->GetIdentifier()); |
| handler_->event_ct_++; |
| |
| Event ev; |
| ev.method = method; |
| if (params) { |
| // Intentionally truncating at small size. |
| ev.params = std::string(static_cast<const char*>(params), |
| std::min(static_cast<int>(params_size), 80)); |
| } |
| handler_->OnEvent(ev); |
| } |
| |
| void OnDevToolsAgentAttached(CefRefPtr<CefBrowser> browser) override { |
| EXPECT_TRUE(browser.get()); |
| EXPECT_EQ(handler_->GetBrowserId(), browser->GetIdentifier()); |
| handler_->attached_ct_++; |
| } |
| |
| void OnDevToolsAgentDetached(CefRefPtr<CefBrowser> browser) override { |
| EXPECT_TRUE(browser.get()); |
| EXPECT_EQ(handler_->GetBrowserId(), browser->GetIdentifier()); |
| handler_->detached_ct_++; |
| } |
| |
| private: |
| DevToolsMessageTestHandler* handler_; |
| |
| IMPLEMENT_REFCOUNTING(TestMessageObserver); |
| DISALLOW_COPY_AND_ASSIGN(TestMessageObserver); |
| }; |
| |
| // Execute a DevTools method. Expected results will be verified in |
| // OnMethodResult, and |next_step| will then be executed. |
| // |expected_result| can be a fragment that the result should start with. |
| void ExecuteMethod(const std::string& method, |
| const std::string& params, |
| const base::Closure& next_step, |
| const std::string& expected_result = "{}", |
| bool expected_success = true) { |
| CHECK(!method.empty()); |
| CHECK(!next_step.is_null()); |
| |
| int message_id = next_message_id_++; |
| |
| std::stringstream message; |
| message << "{\"id\":" << message_id << ",\"method\":\"" << method << "\""; |
| if (!params.empty()) { |
| message << ",\"params\":" << params; |
| } |
| message << "}"; |
| |
| // Set expected result state. |
| pending_message_ = message.str(); |
| pending_result_next_ = next_step; |
| pending_result_ = {message_id, expected_success, expected_result}; |
| |
| if (message_id % 2 == 0) { |
| // Use the less structured method. |
| method_send_ct_++; |
| GetBrowser()->GetHost()->SendDevToolsMessage(pending_message_.data(), |
| pending_message_.size()); |
| } else { |
| // Use the more structured method. |
| method_execute_ct_++; |
| CefRefPtr<CefDictionaryValue> dict; |
| if (!params.empty()) { |
| CefRefPtr<CefValue> value = |
| CefParseJSON(params.data(), params.size(), JSON_PARSER_RFC); |
| EXPECT_TRUE(value && value->GetType() == VTYPE_DICTIONARY) |
| << "failed to parse: " << params; |
| if (value && value->GetType() == VTYPE_DICTIONARY) { |
| dict = value->GetDictionary(); |
| } |
| } |
| GetBrowser()->GetHost()->ExecuteDevToolsMethod(message_id, method, dict); |
| } |
| } |
| |
| // Every call to ExecuteMethod should result in a single call to this method |
| // with the same message_id. |
| void OnMethodResult(const MethodResult& result) { |
| EXPECT_EQ(pending_result_.message_id, result.message_id) |
| << "with message=" << pending_message_; |
| if (result.message_id != pending_result_.message_id) |
| return; |
| |
| EXPECT_EQ(pending_result_.success, result.success) |
| << "with message=" << pending_message_; |
| |
| EXPECT_TRUE(result.result.find(pending_result_.result) == 0) |
| << "with message=" << pending_message_ |
| << "\nand actual result=" << result.result |
| << "\nand expected result=" << pending_result_.result; |
| |
| last_result_id_ = result.message_id; |
| |
| // Continue asynchronously to allow the callstack to unwind. |
| CefPostTask(TID_UI, pending_result_next_); |
| |
| // Clear expected result state. |
| pending_message_.clear(); |
| pending_result_next_.Reset(); |
| pending_result_ = {}; |
| } |
| |
| void OnEvent(const Event& event) { |
| if (event.method != pending_event_.method) |
| return; |
| |
| EXPECT_TRUE(event.params.find(pending_event_.params) == 0) |
| << "with method=" << event.method |
| << "\nand actual params=" << event.params |
| << "\nand expected params=" << pending_event_.params; |
| |
| // Continue asynchronously to allow the callstack to unwind. |
| CefPostTask(TID_UI, pending_event_next_); |
| |
| // Clear expected result state. |
| pending_event_ = {}; |
| pending_event_next_.Reset(); |
| } |
| |
| void Navigate() { |
| pending_event_ = {"Page.frameNavigated", "{\"frame\":"}; |
| pending_event_next_ = |
| base::Bind(&DevToolsMessageTestHandler::AfterNavigate, this); |
| |
| std::stringstream params; |
| params << "{\"url\":\"" << kTestUrl2 << "\"}"; |
| |
| // STEP 3: Page domain notifications are enabled. Now start a new |
| // navigation (but do nothing on method result) and wait for the |
| // "Page.frameNavigated" event. |
| ExecuteMethod("Page.navigate", params.str(), base::Bind(base::DoNothing), |
| /*expected_result=*/"{\"frameId\":"); |
| } |
| |
| void AfterNavigate() { |
| // STEP 4: Got the "Page.frameNavigated" event. Now disable page domain |
| // notifications. |
| ExecuteMethod( |
| "Page.disable", "", |
| base::Bind(&DevToolsMessageTestHandler::AfterPageDisabled, this)); |
| } |
| |
| void AfterPageDisabled() { |
| // STEP 5: Got the the "Page.disable" method result. Now call a non-existant |
| // method to verify an error result, and then destroy the test when done. |
| ExecuteMethod( |
| "Foo.doesNotExist", "", |
| base::Bind(&DevToolsMessageTestHandler::MaybeDestroyTest, this), |
| /*expected_result=*/ |
| "{\"code\":-32601,\"message\":\"'Foo.doesNotExist' wasn't found\"}", |
| /*expected_success=*/false); |
| } |
| |
| void MaybeDestroyTest() { |
| if (result_ct_ == expected_method_ct_ && load_ct_ == expected_load_ct_) { |
| // STEP 6: Got confirmation of all expected method results and load |
| // events. Now destroy the test. |
| DestroyTest(); |
| } |
| } |
| |
| // Total # of times we're planning to call ExecuteMethod. |
| const int expected_method_ct_ = 4; |
| |
| // Total # of times we're expecting OnLoadingStateChange(isLoading=false) to |
| // be called. |
| const int expected_load_ct_ = 2; |
| |
| // In ExecuteMethod: |
| // Next message ID to use. |
| int next_message_id_ = 1; |
| // Last message that was sent (used for debug messages only). |
| std::string pending_message_; |
| // SendDevToolsMessage call count. |
| int method_send_ct_ = 0; |
| // ExecuteDevToolsMethod call count. |
| int method_execute_ct_ = 0; |
| |
| // Expect |pending_result_.message_id| in OnMethodResult. |
| // The result should start with the |pending_result_.result| fragment. |
| MethodResult pending_result_; |
| // Tracks the last message ID received. |
| int last_result_id_ = -1; |
| // When received, execute this callback. |
| base::Closure pending_result_next_; |
| |
| // Wait for |pending_event_.method| in OnEvent. |
| // The params should start with the |pending_event_.params| fragment. |
| Event pending_event_; |
| // When received, execute this callback. |
| base::Closure pending_event_next_; |
| |
| CefRefPtr<CefRegistration> registration_; |
| |
| // Observer callback count. |
| int message_ct_ = 0; |
| int result_ct_ = 0; |
| int event_ct_ = 0; |
| int attached_ct_ = 0; |
| int detached_ct_ = 0; |
| |
| // OnLoadingStateChange(isLoading=false) count. |
| int load_ct_ = 0; |
| |
| TrackCallback observer_destroyed_; |
| |
| IMPLEMENT_REFCOUNTING(DevToolsMessageTestHandler); |
| DISALLOW_COPY_AND_ASSIGN(DevToolsMessageTestHandler); |
| }; |
| |
| } // namespace |
| |
| // Test everything related to DevTools messages: |
| // - CefDevToolsMessageObserver registration and life span. |
| // - SendDevToolsMessage/ExecuteDevToolsMethod calls. |
| // - CefDevToolsMessageObserver callbacks for method results and events. |
| TEST(DevToolsMessageTest, Messages) { |
| CefRefPtr<DevToolsMessageTestHandler> handler = |
| new DevToolsMessageTestHandler(); |
| handler->ExecuteTest(); |
| ReleaseAndWaitForDestructor(handler); |
| } |