blob: 86888061d646a99714bd16f828b8e724e15056f4 [file] [log] [blame]
// Copyright (c) 2010 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/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/automation_messages.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/extension.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 "gfx/rect.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): http://crbug.com/45766
#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_.AppendASCII(extension_path);
launch_arguments_.AppendSwitchPath(switches::kLoadExtension, filename);
functions_enabled_.push_back("*");
}
void set_functions_enabled(
const std::vector<std::string>& functions_enabled) {
functions_enabled_ = functions_enabled;
}
void SetUp() {
ExternalTabUITest::SetUp();
// TabProxy::NavigateInExternalTab is a sync call and can cause a deadlock
// if host window is visible.
mock_->host_window_style_ &= ~WS_VISIBLE;
tab_ = mock_->CreateTabWithUrl(GURL());
tab_->SetEnableExtensionAutomation(functions_enabled_);
}
void TearDown() {
tab_->SetEnableExtensionAutomation(std::vector<std::string>());
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<std::string> functions_enabled_;
// Message loop for running the async bits of your test.
TimedMessageLoopRunner loop_;
// The external tab.
scoped_refptr<TabProxy> 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);
};
// Flaky: http://crbug.com/44599
TEST_F(ExtensionTestSimpleApiCall, FLAKY_RunTest) {
namespace keys = extension_automation_constants;
ASSERT_THAT(mock_, testing::NotNull());
EXPECT_CALL(*mock_, OnDidNavigate(_, _)).Times(1);
EXPECT_CALL(*mock_, OnNavigationStateChanged(_, _, _))
.Times(testing::AnyNumber());
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(action_max_timeout_ms());
ASSERT_FALSE(message_received.empty());
scoped_ptr<Value> message_value(base::JSONReader::Read(message_received,
false));
ASSERT_TRUE(message_value->IsType(Value::TYPE_DICTIONARY));
DictionaryValue* message_dict =
reinterpret_cast<DictionaryValue*>(message_value.get());
std::string result;
ASSERT_TRUE(message_dict->GetString(keys::kAutomationNameKey, &result));
EXPECT_EQ("tabs.remove", result);
result.clear();
ASSERT_TRUE(message_dict->GetString(keys::kAutomationArgsKey, &result));
EXPECT_FALSE(result.empty());
int callback_id = 123456789;
message_dict->GetInteger(keys::kAutomationRequestIdKey, &callback_id);
EXPECT_NE(callback_id, 123456789);
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<Value> message_value(base::JSONReader::Read(message, false));
ASSERT_TRUE(message_value->IsType(Value::TYPE_DICTIONARY));
DictionaryValue* request_dict =
static_cast<DictionaryValue*>(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("tabs.getSelected", function_name);
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.SetBoolean(extension_tabs_module_constants::kIncognitoKey,
false);
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("tabs.remove", function_name);
EXPECT_FALSE(has_callback);
std::string args;
EXPECT_TRUE(request_dict->GetString(keys::kAutomationArgsKey, &args));
EXPECT_NE(std::string::npos, args.find("42"));
loop_.Quit();
} else {
FAIL();
loop_.Quit();
}
}
private:
int messages_received_;
DISALLOW_COPY_AND_ASSIGN(ExtensionTestRoundtripApiCall);
};
// This is flaky on XP Tests (dbg)(2) bot.
// http://code.google.com/p/chromium/issues/detail?id=44650
TEST_F(ExtensionTestRoundtripApiCall, FLAKY_RunTest) {
namespace keys = extension_automation_constants;
ASSERT_THAT(mock_, testing::NotNull());
EXPECT_CALL(*mock_, OnDidNavigate(_, _)).Times(1);
EXPECT_CALL(*mock_, OnNavigationStateChanged(_, _, _))
.Times(testing::AnyNumber());
EXPECT_CALL(*mock_, OnLoad(_, _)).Times(testing::AnyNumber());
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(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_message) {
// JSON doesn't allow single quotes.
std::string event_message_strict(event_message);
ReplaceSubstringsAfterOffset(&event_message_strict, 0, "\'", "\\\"");
namespace keys = extension_automation_constants;
ASSERT_TRUE(tab_ != NULL);
tab_->HandleMessageFromExternalHost(
event_message_strict,
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<std::string, int> 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,'incognito':false,'type':'normal'}]\"]",
"[\"windows.onRemoved\", \"[42]\"]",
"[\"windows.onFocusChanged\", \"[42]\"]",
// Tab events.
"[\"tabs.onCreated\", \"[{'id':42,'index':1,'windowId':1,"
"'selected':true,'url':'http://www.google.com','incognito':false}]\"]",
"[\"tabs.onUpdated\", \"[42, {'status': 'complete',"
"'url':'http://www.google.com'}, {'id':42,'index':1,'windowId':1,"
"'selected':true,'url':'http://www.google.com','incognito':false}]\"]",
"[\"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_FALSE(message.empty());
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<Value> message_value(base::JSONReader::Read(message, false));
ASSERT_TRUE(message_value->IsType(Value::TYPE_DICTIONARY));
DictionaryValue* message_dict =
reinterpret_cast<DictionaryValue*>(message_value.get());
std::string name;
ASSERT_TRUE(message_dict->GetString(keys::kAutomationNameKey, &name));
ASSERT_EQ("windows.getCurrent", name);
// 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 (size_t 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<Value> message_value(base::JSONReader::Read(message, false));
ASSERT_TRUE(message_value->IsType(Value::TYPE_DICTIONARY));
DictionaryValue* message_dict =
reinterpret_cast<DictionaryValue*>(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();
}
}
}
// Flaky, http://crbug.com/37554.
TEST_F(ExtensionTestBrowserEvents, FLAKY_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_, OnNavigationStateChanged(_, _, _))
.Times(testing::AnyNumber());
EXPECT_CALL(*mock_, OnLoad(_, _)).Times(testing::AnyNumber());
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(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<std::string, int>::iterator i = event_count_.begin();
i != event_count_.end(); ++i) {
const std::pair<std::string, int>& value = *i;
EXPECT_EQ(1, value.second);
}
}
#endif // defined(OS_WIN)
} // namespace