// Copyright (c) 2009 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 "base/command_line.h" #include "base/gfx/rect.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" #include "base/values.h" #include "chrome/browser/automation/extension_automation_constants.h" #include "chrome/browser/extensions/extension_tabs_module_constants.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/extensions/extension.h" #include "chrome/test/automation/automation_messages.h" #include "chrome/test/automation/automation_proxy_uitest.h" #include "chrome/test/automation/tab_proxy.h" #include "chrome/test/ui/ui_test.h" #include "chrome/test/ui_test_utils.h" #include "googleurl/src/gurl.h" #define GMOCK_MUTANT_INCLUDE_LATE_OBJECT_BINDING #include "testing/gmock_mutant.h" namespace { using testing::_; using testing::CreateFunctor; using testing::DoAll; using testing::Invoke; using testing::InvokeWithoutArgs; using testing::SaveArg; using testing::WithArgs; using ui_test_utils::TimedMessageLoopRunner; static const char kTestDirectorySimpleApiCall[] = "extensions/uitest/simple_api_call"; static const char kTestDirectoryRoundtripApiCall[] = "extensions/uitest/roundtrip_api_call"; static const char kTestDirectoryBrowserEvent[] = "extensions/uitest/event_sink"; // TODO(port) Once external tab stuff is ported. #if defined(OS_WIN) // Base class to test extensions almost end-to-end by including browser // startup, manifest parsing, and the actual process model in the // equation. This would also let you write UITests that test individual // Chrome Extensions as running in Chrome. Takes over implementation of // extension API calls so that behavior can be tested deterministically // through code, instead of having to contort the browser into a state // suitable for testing. // // By default, makes Chrome forward all Chrome Extension API function calls // via the automation interface. To override this, call set_functions_enabled() // with a list of function names that should be forwarded, class ExtensionUITest : public ExternalTabUITest { public: explicit ExtensionUITest(const std::string& extension_path) : loop_(MessageLoop::current()) { FilePath filename(test_data_directory_); filename = filename.AppendASCII(extension_path); launch_arguments_.AppendSwitchWithValue(switches::kLoadExtension, filename.value()); functions_enabled_.push_back("*"); } void set_functions_enabled( const std::vector& functions_enabled) { functions_enabled_ = functions_enabled; } void SetUp() { ExternalTabUITest::SetUp(); tab_ = mock_->CreateTabWithUrl(GURL()); tab_->SetEnableExtensionAutomation(functions_enabled_); } void TearDown() { tab_->SetEnableExtensionAutomation(std::vector()); tab_ = NULL; EXPECT_TRUE(mock_->HostWindowExists()) << "You shouldn't DestroyHostWindow yourself, or extension automation " "won't be correctly reset. Just exit your message loop."; mock_->DestroyHostWindow(); ExternalTabUITest::TearDown(); } protected: // Extension API functions that we want to take over. Defaults to all. std::vector functions_enabled_; // Message loop for running the async bits of your test. TimedMessageLoopRunner loop_; // The external tab. scoped_refptr tab_; private: DISALLOW_COPY_AND_ASSIGN(ExtensionUITest); }; // A test that loads a basic extension that makes an API call that does // not require a response. class ExtensionTestSimpleApiCall : public ExtensionUITest { public: ExtensionTestSimpleApiCall() : ExtensionUITest(kTestDirectorySimpleApiCall) { } void SetUp() { // Set just this one function explicitly to be forwarded, as a test of // the selective forwarding. The next test will leave the default to test // universal forwarding. functions_enabled_.clear(); functions_enabled_.push_back("tabs.remove"); ExtensionUITest::SetUp(); } private: DISALLOW_COPY_AND_ASSIGN(ExtensionTestSimpleApiCall); }; TEST_F(ExtensionTestSimpleApiCall, RunTest) { namespace keys = extension_automation_constants; ASSERT_THAT(mock_, testing::NotNull()); EXPECT_CALL(*mock_, OnDidNavigate(_, _)).Times(1); std::string message_received; EXPECT_CALL(*mock_, OnForwardMessageToExternalHost( _, _, keys::kAutomationOrigin, keys::kAutomationRequestTarget)) .WillOnce(DoAll( SaveArg<1>(&message_received), InvokeWithoutArgs( CreateFunctor(&loop_, &TimedMessageLoopRunner::Quit)))); EXPECT_CALL(*mock_, HandleClosed(_)); ASSERT_EQ(AUTOMATION_MSG_NAVIGATION_SUCCESS, tab_->NavigateInExternalTab( GURL("chrome-extension://pmgpglkggjdpkpghhdmbdhababjpcohk/test.html"), GURL(""))); loop_.RunFor(2 * action_max_timeout_ms()); ASSERT_FALSE(message_received.empty()); scoped_ptr message_value(base::JSONReader::Read(message_received, false)); ASSERT_TRUE(message_value->IsType(Value::TYPE_DICTIONARY)); DictionaryValue* message_dict = reinterpret_cast(message_value.get()); std::string result; ASSERT_TRUE(message_dict->GetString(keys::kAutomationNameKey, &result)); EXPECT_EQ(result, "tabs.remove"); result = ""; ASSERT_TRUE(message_dict->GetString(keys::kAutomationArgsKey, &result)); EXPECT_NE(result, ""); int callback_id = 0xBAADF00D; message_dict->GetInteger(keys::kAutomationRequestIdKey, &callback_id); EXPECT_NE(callback_id, 0xBAADF00D); bool has_callback = true; EXPECT_TRUE(message_dict->GetBoolean(keys::kAutomationHasCallbackKey, &has_callback)); EXPECT_FALSE(has_callback); } // A test that loads a basic extension that makes an API call that does // not require a response. class ExtensionTestRoundtripApiCall : public ExtensionUITest { public: ExtensionTestRoundtripApiCall() : ExtensionUITest(kTestDirectoryRoundtripApiCall), messages_received_(0) { } void SetUp() { // Set just this one function explicitly to be forwarded, as a test of // the selective forwarding. The next test will leave the default to test // universal forwarding. functions_enabled_.clear(); functions_enabled_.push_back("tabs.getSelected"); functions_enabled_.push_back("tabs.remove"); ExtensionUITest::SetUp(); } void CheckAndSendResponse(const std::string& message) { namespace keys = extension_automation_constants; ++messages_received_; ASSERT_TRUE(tab_ != NULL); ASSERT_TRUE(messages_received_ == 1 || messages_received_ == 2); scoped_ptr message_value(base::JSONReader::Read(message, false)); ASSERT_TRUE(message_value->IsType(Value::TYPE_DICTIONARY)); DictionaryValue* request_dict = static_cast(message_value.get()); std::string function_name; ASSERT_TRUE(request_dict->GetString(keys::kAutomationNameKey, &function_name)); int request_id = -2; EXPECT_TRUE(request_dict->GetInteger(keys::kAutomationRequestIdKey, &request_id)); bool has_callback = false; EXPECT_TRUE(request_dict->GetBoolean(keys::kAutomationHasCallbackKey, &has_callback)); if (messages_received_ == 1) { EXPECT_EQ(function_name, "tabs.getSelected"); EXPECT_GE(request_id, 0); EXPECT_TRUE(has_callback); DictionaryValue response_dict; response_dict.SetInteger(keys::kAutomationRequestIdKey, request_id); DictionaryValue tab_dict; tab_dict.SetInteger(extension_tabs_module_constants::kIdKey, 42); tab_dict.SetInteger(extension_tabs_module_constants::kIndexKey, 1); tab_dict.SetInteger(extension_tabs_module_constants::kWindowIdKey, 1); tab_dict.SetBoolean(extension_tabs_module_constants::kSelectedKey, true); tab_dict.SetString(extension_tabs_module_constants::kUrlKey, "http://www.google.com"); std::string tab_json; base::JSONWriter::Write(&tab_dict, false, &tab_json); response_dict.SetString(keys::kAutomationResponseKey, tab_json); std::string response_json; base::JSONWriter::Write(&response_dict, false, &response_json); tab_->HandleMessageFromExternalHost( response_json, keys::kAutomationOrigin, keys::kAutomationResponseTarget); } else if (messages_received_ == 2) { EXPECT_EQ(function_name, "tabs.remove"); EXPECT_FALSE(has_callback); std::string args; EXPECT_TRUE(request_dict->GetString(keys::kAutomationArgsKey, &args)); EXPECT_NE(args.find("42"), -1); loop_.Quit(); } else { FAIL(); loop_.Quit(); } } private: int messages_received_; DISALLOW_COPY_AND_ASSIGN(ExtensionTestRoundtripApiCall); }; TEST_F(ExtensionTestRoundtripApiCall, RunTest) { namespace keys = extension_automation_constants; ASSERT_THAT(mock_, testing::NotNull()); EXPECT_CALL(*mock_, OnDidNavigate(_, _)).Times(1); EXPECT_CALL(*mock_, OnForwardMessageToExternalHost( _, _, keys::kAutomationOrigin, keys::kAutomationRequestTarget)) .Times(2) .WillRepeatedly(WithArgs<1>(Invoke( CreateFunctor(this, &ExtensionTestRoundtripApiCall::CheckAndSendResponse)))); EXPECT_CALL(*mock_, HandleClosed(_)); ASSERT_EQ(AUTOMATION_MSG_NAVIGATION_SUCCESS, tab_->NavigateInExternalTab( GURL("chrome-extension://ofoknjclcmghjfmbncljcnpjmfmldhno/test.html"), GURL(""))); // CheckAndSendResponse (called by OnForwardMessageToExternalHost) // will end the loop once it has received both of our expected messages. loop_.RunFor(2 * action_max_timeout_ms()); } class ExtensionTestBrowserEvents : public ExtensionUITest { public: ExtensionTestBrowserEvents() : ExtensionUITest(kTestDirectoryBrowserEvent), response_count_(0) { } void SetUp() { // Set just this one function explicitly to be forwarded, as a test of // the selective forwarding. The next test will leave the default to test // universal forwarding. functions_enabled_.clear(); functions_enabled_.push_back("windows.getCurrent"); ExtensionUITest::SetUp(); } // Fire an event of the given name to the test extension. void FireEvent(const char* event_name) { namespace keys = extension_automation_constants; ASSERT_TRUE(tab_ != NULL); // Build the event message to send to the extension. The only important // part is the name, as the payload is not used by the test extension. std::string message; message += event_name; tab_->HandleMessageFromExternalHost( message, keys::kAutomationOrigin, keys::kAutomationBrowserEventRequestTarget); } void HandleMessageFromChrome(const std::string& message, const std::string& target); protected: // Counts the number of times we got a given event. std::map event_count_; // Array containing the names of the events to fire to the extension. static const char* events_[]; // Number of events extension has told us it received. int response_count_; DISALLOW_COPY_AND_ASSIGN(ExtensionTestBrowserEvents); }; const char* ExtensionTestBrowserEvents::events_[] = { // Window events. "[\"windows.onCreated\", \"[{'id':42,'focused':true,'top':0,'left':0," "'width':100,'height':100}]\"]", "[\"windows.onRemoved\", \"[42]\"]", "[\"windows.onFocusChanged\", \"[42]\"]", // Tab events. "[\"tabs.onCreated\", \"[{'id\':42,'index':1,'windowId':1," "'selected':true,'url':'http://www.google.com'}]\"]", "[\"tabs.onUpdated\", \"[42, {'status': 'complete'," "'url':'http://www.google.com'}, {'id\':42,'index':1,'windowId':1," "'selected':true,'url':'http://www.google.com'}]\"]", "[\"tabs.onMoved\", \"[42, {'windowId':1,'fromIndex':1,'toIndex':2}]\"]", "[\"tabs.onSelectionChanged\", \"[42, {'windowId':1}]\"]", "[\"tabs.onAttached\", \"[42, {'newWindowId':1,'newPosition':1}]\"]", "[\"tabs.onDetached\", \"[43, {'oldWindowId':1,'oldPosition':1}]\"]", "[\"tabs.onRemoved\", \"[43]\"]", // Bookmark events. "[\"bookmarks.onCreated\", \"['42', {'id':'42','title':'foo',}]\"]", "[\"bookmarks.onRemoved\", \"['42', {'parentId':'2','index':1}]\"]", "[\"bookmarks.onChanged\", \"['42', {'title':'foo'}]\"]", "[\"bookmarks.onMoved\", \"['42', {'parentId':'2','index':1," "'oldParentId':'3','oldIndex':2}]\"]", "[\"bookmarks.onChildrenReordered\", \"['32', " "{'childIds':['1', '2', '3']}]\"]" }; void ExtensionTestBrowserEvents::HandleMessageFromChrome( const std::string& message, const std::string& target) { namespace keys = extension_automation_constants; ASSERT_TRUE(tab_ != NULL); ASSERT_TRUE(message.length() > 0); if (target == keys::kAutomationRequestTarget) { // This should be a request for the current window. We don't need to // respond, as this is used only as an indication that the extension // page is now loaded. scoped_ptr message_value(base::JSONReader::Read(message, false)); ASSERT_TRUE(message_value->IsType(Value::TYPE_DICTIONARY)); DictionaryValue* message_dict = reinterpret_cast(message_value.get()); std::string name; ASSERT_TRUE(message_dict->GetString(keys::kAutomationNameKey, &name)); ASSERT_STREQ(name.c_str(), "windows.getCurrent"); // Send an OpenChannelToExtension message to chrome. Note: the JSON // reader expects quoted property keys. See the comment in // TEST_F(BrowserEventExtensionTest, RunTest) to understand where the // extension Id comes from. tab_->HandleMessageFromExternalHost( "{\"rqid\":0, \"extid\": \"ofoknjclcmghjfmbncljcnpjmfmldhno\"," " \"connid\": 1}", keys::kAutomationOrigin, keys::kAutomationPortRequestTarget); } else if (target == keys::kAutomationPortResponseTarget) { // This is a response to the open channel request. This means we know // that the port is ready to send us messages. Fire all the events now. for (int i = 0; i < arraysize(events_); ++i) { FireEvent(events_[i]); } } else if (target == keys::kAutomationPortRequestTarget) { // This is the test extension calling us back. Make sure its telling // us that it received an event. We do this by checking to see if the // message is a simple string of one of the event names that is fired. // // There is a special message "ACK" which means that the extension // received the port connection. This is not an event response and // should happen before all events. scoped_ptr message_value(base::JSONReader::Read(message, false)); ASSERT_TRUE(message_value->IsType(Value::TYPE_DICTIONARY)); DictionaryValue* message_dict = reinterpret_cast(message_value.get()); std::string event_name; message_dict->GetString(keys::kAutomationMessageDataKey, &event_name); if (event_name == "\"ACK\"") { ASSERT_EQ(0, event_count_.size()); } else if (event_name.empty()) { // This must be the post disconnect. int request_id = -1; message_dict->GetInteger(keys::kAutomationRequestIdKey, &request_id); ASSERT_EQ(keys::CHANNEL_CLOSED, request_id); } else { ++event_count_[event_name]; } // Check if we're done. if (event_count_.size() == arraysize(events_)) { loop_.Quit(); } } } TEST_F(ExtensionTestBrowserEvents, RunTest) { // This test loads an HTML file that tries to add listeners to a bunch of // chrome.* events and upon adding a listener it posts the name of the event // to the automation layer, which we'll count to make sure the events work. namespace keys = extension_automation_constants; ASSERT_THAT(mock_, testing::NotNull()); EXPECT_CALL(*mock_, OnDidNavigate(_, _)).Times(1); EXPECT_CALL(*mock_, OnForwardMessageToExternalHost( _, _, keys::kAutomationOrigin, _)) .WillRepeatedly(WithArgs<1, 3>(Invoke( CreateFunctor(this, &ExtensionTestBrowserEvents::HandleMessageFromChrome)))); EXPECT_CALL(*mock_, HandleClosed(_)); ASSERT_EQ(AUTOMATION_MSG_NAVIGATION_SUCCESS, tab_->NavigateInExternalTab( GURL("chrome-extension://ofoknjclcmghjfmbncljcnpjmfmldhno/test.html"), GURL(""))); // HandleMessageFromChrome (called by OnForwardMessageToExternalHost) ends // the loop when we've received the number of response messages we expect. loop_.RunFor(2 * action_max_timeout_ms()); // If this assert hits and the actual size is 0 then you need to look at: // src\chrome\test\data\extensions\uitest\event_sink\test.html and see if // all the events we are attaching to are valid. Also compare the list against // the event_names_ string array above. ASSERT_EQ(arraysize(events_), event_count_.size()); for (std::map::iterator i = event_count_.begin(); i != event_count_.end(); ++i) { const std::pair& value = *i; EXPECT_EQ(1, value.second); } } #endif // defined(OS_WIN) } // namespace