summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorakalin@chromium.org <akalin@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-02-01 07:42:12 +0000
committerakalin@chromium.org <akalin@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-02-01 07:42:12 +0000
commitf92351d68c2089e7e36c00a4789ad3a199921ebc (patch)
tree49f4e1438481c33f37338178ca0515b597173b2d
parent14c5c9c368d6f38c09b7cf3d170b2b55caf93ffd (diff)
downloadchromium_src-f92351d68c2089e7e36c00a4789ad3a199921ebc.zip
chromium_src-f92351d68c2089e7e36c00a4789ad3a199921ebc.tar.gz
chromium_src-f92351d68c2089e7e36c00a4789ad3a199921ebc.tar.bz2
[Sync] Add JS scriptability to syncapi
Add plumbing for ProfileSyncService, SyncBackendHost, and SyncManager to be able to listen for messages and emit events to chrome://sync-internals. Add initial version of notification-related messages and events and hook it up to chrome://sync-internals. Added rudimentary display for notifications to chrome://sync-internals. Changed DOMUI class slightly to be more testable. BUG=69500 TEST= Review URL: http://codereview.chromium.org/6379010 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@73263 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/browser/dom_ui/dom_ui.cc12
-rw-r--r--chrome/browser/dom_ui/dom_ui.h7
-rw-r--r--chrome/browser/dom_ui/sync_internals_message_handler.cc48
-rw-r--r--chrome/browser/dom_ui/sync_internals_message_handler.h39
-rw-r--r--chrome/browser/dom_ui/sync_internals_ui.cc77
-rw-r--r--chrome/browser/dom_ui/sync_internals_ui.h34
-rw-r--r--chrome/browser/dom_ui/sync_internals_ui_unittest.cc217
-rw-r--r--chrome/browser/resources/sync_internals/sync_index.html51
-rw-r--r--chrome/browser/sync/README.js54
-rw-r--r--chrome/browser/sync/engine/syncapi.cc92
-rw-r--r--chrome/browser/sync/engine/syncapi.h31
-rw-r--r--chrome/browser/sync/engine/syncapi_unittest.cc165
-rw-r--r--chrome/browser/sync/glue/sync_backend_host.cc92
-rw-r--r--chrome/browser/sync/glue/sync_backend_host.h49
-rw-r--r--chrome/browser/sync/js_arg_list.cc53
-rw-r--r--chrome/browser/sync/js_arg_list.h54
-rw-r--r--chrome/browser/sync/js_arg_list_unittest.cc63
-rw-r--r--chrome/browser/sync/js_backend.h44
-rw-r--r--chrome/browser/sync/js_event_handler.h30
-rw-r--r--chrome/browser/sync/js_event_handler_list.cc94
-rw-r--r--chrome/browser/sync/js_event_handler_list.h76
-rw-r--r--chrome/browser/sync/js_event_handler_list_unittest.cc114
-rw-r--r--chrome/browser/sync/js_event_router.h37
-rw-r--r--chrome/browser/sync/js_frontend.h43
-rw-r--r--chrome/browser/sync/js_test_util.cc60
-rw-r--r--chrome/browser/sync/js_test_util.h70
-rw-r--r--chrome/browser/sync/profile_sync_service.cc16
-rw-r--r--chrome/browser/sync/profile_sync_service.h12
-rw-r--r--chrome/browser/sync/profile_sync_service_mock.h1
-rw-r--r--chrome/browser/sync/profile_sync_service_unittest.cc188
-rw-r--r--chrome/browser/sync/test_profile_sync_service.cc34
-rw-r--r--chrome/browser/sync/test_profile_sync_service.h16
-rw-r--r--chrome/chrome.gyp8
-rw-r--r--chrome/chrome_browser.gypi2
-rw-r--r--chrome/chrome_tests.gypi21
-rw-r--r--chrome/test/profile_mock.h1
36 files changed, 1875 insertions, 130 deletions
diff --git a/chrome/browser/dom_ui/dom_ui.cc b/chrome/browser/dom_ui/dom_ui.cc
index a96521e..98d4bf2 100644
--- a/chrome/browser/dom_ui/dom_ui.cc
+++ b/chrome/browser/dom_ui/dom_ui.cc
@@ -134,18 +134,16 @@ Profile* DOMUI::GetProfile() const {
return tab_contents()->profile();
}
-// DOMUI, protected: ----------------------------------------------------------
-
-void DOMUI::AddMessageHandler(DOMMessageHandler* handler) {
- handlers_.push_back(handler);
-}
-
RenderViewHost* DOMUI::GetRenderViewHost() const {
DCHECK(tab_contents());
return tab_contents()->render_view_host();
}
-// DOMUI, private: ------------------------------------------------------------
+// DOMUI, protected: ----------------------------------------------------------
+
+void DOMUI::AddMessageHandler(DOMMessageHandler* handler) {
+ handlers_.push_back(handler);
+}
void DOMUI::ExecuteJavascript(const std::wstring& javascript) {
GetRenderViewHost()->ExecuteJavascriptInWebFrame(std::wstring(), javascript);
diff --git a/chrome/browser/dom_ui/dom_ui.h b/chrome/browser/dom_ui/dom_ui.h
index f2463ab..67932ef 100644
--- a/chrome/browser/dom_ui/dom_ui.h
+++ b/chrome/browser/dom_ui/dom_ui.h
@@ -141,6 +141,10 @@ class DOMUI {
protected:
void AddMessageHandler(DOMMessageHandler* handler);
+ // Execute a string of raw Javascript on the page. Overridable for
+ // testing purposes.
+ virtual void ExecuteJavascript(const std::wstring& javascript);
+
// Options that may be overridden by individual DOM UI implementations. The
// bool options default to false. See the public getters for more information.
bool hide_favicon_;
@@ -159,9 +163,6 @@ class DOMUI {
TabContents* tab_contents_;
private:
- // Execute a string of raw Javascript on the page.
- void ExecuteJavascript(const std::wstring& javascript);
-
// A map of message name -> message handling callback.
typedef std::map<std::string, MessageCallback*> MessageCallbackMap;
MessageCallbackMap message_callbacks_;
diff --git a/chrome/browser/dom_ui/sync_internals_message_handler.cc b/chrome/browser/dom_ui/sync_internals_message_handler.cc
deleted file mode 100644
index 5ef0084..0000000
--- a/chrome/browser/dom_ui/sync_internals_message_handler.cc
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) 2011 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 "chrome/browser/dom_ui/sync_internals_message_handler.h"
-
-#include "base/callback.h"
-#include "base/logging.h"
-#include "base/values.h"
-#include "chrome/browser/profiles/profile.h"
-#include "chrome/browser/sync/profile_sync_service.h"
-#include "chrome/browser/sync/sync_ui_util.h"
-
-SyncInternalsMessageHandler::SyncInternalsMessageHandler(Profile* profile)
- : profile_(profile) {
- DCHECK(profile_);
- ProfileSyncService* service = profile_->GetProfileSyncService();
- if (service) {
- service->AddObserver(this);
- }
- // TODO(akalin): Listen for when the service gets created/destroyed.
-}
-
-SyncInternalsMessageHandler::~SyncInternalsMessageHandler() {
- ProfileSyncService* service = profile_->GetProfileSyncService();
- if (service) {
- service->RemoveObserver(this);
- }
-}
-
-void SyncInternalsMessageHandler::OnStateChanged() {
- dom_ui_->CallJavascriptFunction(L"onSyncServiceStateChanged");
-}
-
-void SyncInternalsMessageHandler::RegisterMessages() {
- dom_ui_->RegisterMessageCallback(
- "getAboutInfo",
- NewCallback(
- this, &SyncInternalsMessageHandler::HandleGetAboutInfo));
-}
-
-void SyncInternalsMessageHandler::HandleGetAboutInfo(const ListValue* args) {
- ProfileSyncService* service = profile_->GetProfileSyncService();
- DictionaryValue about_info;
- sync_ui_util::ConstructAboutInformation(service, &about_info);
-
- dom_ui_->CallJavascriptFunction(L"onGetAboutInfoFinished", about_info);
-}
diff --git a/chrome/browser/dom_ui/sync_internals_message_handler.h b/chrome/browser/dom_ui/sync_internals_message_handler.h
deleted file mode 100644
index 341750e..0000000
--- a/chrome/browser/dom_ui/sync_internals_message_handler.h
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) 2011 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.
-
-#ifndef CHROME_BROWSER_DOM_UI_SYNC_INTERNALS_MESSAGE_HANDLER_H_
-#define CHROME_BROWSER_DOM_UI_SYNC_INTERNALS_MESSAGE_HANDLER_H_
-#pragma once
-
-#include "base/basictypes.h"
-#include "chrome/browser/dom_ui/dom_ui.h"
-#include "chrome/browser/sync/profile_sync_service_observer.h"
-
-class Profile;
-
-class SyncInternalsMessageHandler : public DOMMessageHandler,
- public ProfileSyncServiceObserver {
- public:
- // Does not take ownership of |profile|, which must outlive this
- // object.
- explicit SyncInternalsMessageHandler(Profile* profile);
- virtual ~SyncInternalsMessageHandler();
-
- // ProfileSyncServiceObserver implementation.
- virtual void OnStateChanged();
-
- protected:
- // DOMMessageHandler implementation.
- virtual void RegisterMessages();
-
- private:
- // Callback handlers.
- void HandleGetAboutInfo(const ListValue* args);
-
- Profile* profile_;
-
- DISALLOW_COPY_AND_ASSIGN(SyncInternalsMessageHandler);
-};
-
-#endif // CHROME_BROWSER_DOM_UI_SYNC_INTERNALS_MESSAGE_HANDLER_H_
diff --git a/chrome/browser/dom_ui/sync_internals_ui.cc b/chrome/browser/dom_ui/sync_internals_ui.cc
index 838f5d6..0af9f0c 100644
--- a/chrome/browser/dom_ui/sync_internals_ui.cc
+++ b/chrome/browser/dom_ui/sync_internals_ui.cc
@@ -4,22 +4,33 @@
#include "chrome/browser/dom_ui/sync_internals_ui.h"
+#include <string>
+
+#include "base/logging.h"
#include "base/ref_counted.h"
#include "base/task.h"
#include "base/tracked_objects.h"
+#include "base/values.h"
#include "chrome/browser/browser_thread.h"
#include "chrome/browser/dom_ui/chrome_url_data_manager.h"
#include "chrome/browser/dom_ui/sync_internals_html_source.h"
-#include "chrome/browser/dom_ui/sync_internals_message_handler.h"
-#include "chrome/browser/tab_contents/tab_contents.h"
-
-SyncInternalsUI::SyncInternalsUI(TabContents* contents) : DOMUI(contents) {
- SyncInternalsMessageHandler* message_handler =
- new SyncInternalsMessageHandler(contents->profile());
- message_handler->Attach(this);
- AddMessageHandler(message_handler);
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/sync/js_arg_list.h"
+#include "chrome/browser/sync/js_frontend.h"
+#include "chrome/browser/sync/profile_sync_service.h"
+#include "chrome/browser/sync/sync_ui_util.h"
+#include "chrome/common/render_messages_params.h"
- BrowserThread::PostTask(
+SyncInternalsUI::SyncInternalsUI(TabContents* contents)
+ : DOMUI(contents) {
+ browser_sync::JsFrontend* backend = GetJsFrontend();
+ if (backend) {
+ backend->AddHandler(this);
+ }
+ // If this PostTask() call fails, it's most likely because this is
+ // being run from a unit test. The created objects will be cleaned
+ // up, anyway.
+ (void)BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE,
NewRunnableMethod(
ChromeURLDataManager::GetInstance(),
@@ -27,4 +38,50 @@ SyncInternalsUI::SyncInternalsUI(TabContents* contents) : DOMUI(contents) {
make_scoped_refptr(new SyncInternalsHTMLSource())));
}
-SyncInternalsUI::~SyncInternalsUI() {}
+SyncInternalsUI::~SyncInternalsUI() {
+ browser_sync::JsFrontend* backend = GetJsFrontend();
+ if (backend) {
+ backend->RemoveHandler(this);
+ }
+}
+
+void SyncInternalsUI::ProcessDOMUIMessage(
+ const ViewHostMsg_DomMessage_Params& params) {
+ const std::string& name = params.name;
+ browser_sync::JsArgList args(params.arguments);
+ VLOG(1) << "Received message: " << name << " with args "
+ << args.ToString();
+ // We handle this case directly because it needs to work even if
+ // the sync service doesn't exist.
+ if (name == "getAboutInfo") {
+ ListValue args;
+ DictionaryValue* about_info = new DictionaryValue();
+ args.Append(about_info);
+ ProfileSyncService* service = GetProfile()->GetProfileSyncService();
+ sync_ui_util::ConstructAboutInformation(service, about_info);
+ HandleJsEvent("onGetAboutInfoFinished",
+ browser_sync::JsArgList(args));
+ } else {
+ browser_sync::JsFrontend* backend = GetJsFrontend();
+ if (backend) {
+ backend->ProcessMessage(name, args, this);
+ } else {
+ LOG(WARNING) << "No sync service; dropping message " << name
+ << " with args " << args.ToString();
+ }
+ }
+}
+
+void SyncInternalsUI::HandleJsEvent(const std::string& name,
+ const browser_sync::JsArgList& args) {
+ VLOG(1) << "Handling event: " << name << " with args " << args.ToString();
+ std::vector<const Value*> arg_list(args.Get().begin(), args.Get().end());
+ CallJavascriptFunction(UTF8ToWide(name), arg_list);
+}
+
+browser_sync::JsFrontend* SyncInternalsUI::GetJsFrontend() {
+ // If this returns NULL that means that sync is disabled for
+ // whatever reason.
+ ProfileSyncService* sync_service = GetProfile()->GetProfileSyncService();
+ return sync_service ? sync_service->GetJsFrontend() : NULL;
+}
diff --git a/chrome/browser/dom_ui/sync_internals_ui.h b/chrome/browser/dom_ui/sync_internals_ui.h
index d17f34a..1bc12a4 100644
--- a/chrome/browser/dom_ui/sync_internals_ui.h
+++ b/chrome/browser/dom_ui/sync_internals_ui.h
@@ -6,15 +6,47 @@
#define CHROME_BROWSER_DOM_UI_SYNC_INTERNALS_UI_H_
#pragma once
+#include <string>
+
#include "base/basictypes.h"
#include "chrome/browser/dom_ui/dom_ui.h"
+#include "chrome/browser/sync/js_event_handler.h"
+
+namespace browser_sync {
+class JsFrontend;
+} // namespace browser_sync
-class SyncInternalsUI : public DOMUI {
+// The implementation for the chrome://sync-internals page.
+class SyncInternalsUI : public DOMUI, public browser_sync::JsEventHandler {
public:
explicit SyncInternalsUI(TabContents* contents);
virtual ~SyncInternalsUI();
+ // DOMUI implementation.
+ //
+ // The following messages are processed:
+ //
+ // getAboutInfo():
+ // Immediately fires a onGetAboutInfoFinished() event with a
+ // dictionary of sync-related stats and info.
+ //
+ // All other messages are routed to the sync service if it exists,
+ // and dropped otherwise.
+ //
+ // TODO(akalin): Add a simple isSyncEnabled() message and make
+ // getAboutInfo() be handled by the sync service.
+ virtual void ProcessDOMUIMessage(
+ const ViewHostMsg_DomMessage_Params& params);
+
+ // browser_sync::JsEventHandler implementation.
+ virtual void HandleJsEvent(const std::string& name,
+ const browser_sync::JsArgList& args);
+
private:
+ // Returns the sync service's JsFrontend object, or NULL if the sync
+ // service does not exist.
+ browser_sync::JsFrontend* GetJsFrontend();
+
DISALLOW_COPY_AND_ASSIGN(SyncInternalsUI);
};
diff --git a/chrome/browser/dom_ui/sync_internals_ui_unittest.cc b/chrome/browser/dom_ui/sync_internals_ui_unittest.cc
new file mode 100644
index 0000000..c36bfef
--- /dev/null
+++ b/chrome/browser/dom_ui/sync_internals_ui_unittest.cc
@@ -0,0 +1,217 @@
+// Copyright (c) 2011 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 "chrome/browser/dom_ui/sync_internals_ui.h"
+
+#include <cstddef>
+#include <string>
+
+#include "base/message_loop.h"
+#include "base/values.h"
+#include "chrome/browser/browser_thread.h"
+#include "chrome/browser/sync/js_arg_list.h"
+#include "chrome/browser/sync/js_test_util.h"
+#include "chrome/browser/sync/profile_sync_service_mock.h"
+#include "chrome/browser/tab_contents/test_tab_contents.h"
+#include "chrome/common/render_messages_params.h"
+#include "chrome/test/profile_mock.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+using browser_sync::HasArgsAsList;
+using browser_sync::JsArgList;
+using testing::NiceMock;
+using testing::Return;
+using testing::StrictMock;
+
+// Subclass of SyncInternalsUI to mock out ExecuteJavascript.
+class TestSyncInternalsUI : public SyncInternalsUI {
+ public:
+ explicit TestSyncInternalsUI(TabContents* contents)
+ : SyncInternalsUI(contents) {}
+ virtual ~TestSyncInternalsUI() {}
+
+ MOCK_METHOD1(ExecuteJavascript, void(const std::wstring&));
+};
+
+class SyncInternalsUITest : public testing::Test {
+ protected:
+ // We allocate memory for |sync_internals_ui_| but we don't
+ // construct it. This is because we want to set mock expectations
+ // with its address before we construct it, and its constructor
+ // calls into our mocks.
+ SyncInternalsUITest()
+ : ui_thread_(BrowserThread::UI, MessageLoopForUI::current()),
+ test_tab_contents_(&profile_mock_, NULL),
+ test_sync_internals_ui_buf_(
+ operator new(sizeof(TestSyncInternalsUI))),
+ test_sync_internals_ui_constructor_called_(false) {}
+
+ virtual ~SyncInternalsUITest() {
+ if (test_sync_internals_ui_constructor_called_) {
+ GetTestSyncInternalsUI()->~TestSyncInternalsUI();
+ }
+ operator delete(test_sync_internals_ui_buf_);
+ }
+
+ // Set up boilerplate expectations for calls done during
+ // SyncInternalUI's construction/destruction.
+ void ExpectSetupTeardownCalls() {
+ EXPECT_CALL(profile_mock_, GetProfileSyncService())
+ .WillRepeatedly(Return(&profile_sync_service_mock_));
+
+ EXPECT_CALL(profile_sync_service_mock_, GetJsFrontend())
+ .WillRepeatedly(Return(&mock_js_backend_));
+
+ // Called by sync_ui_util::ConstructAboutInformation().
+ EXPECT_CALL(profile_sync_service_mock_, HasSyncSetupCompleted())
+ .WillRepeatedly(Return(false));
+
+ // Called by SyncInternalsUI's constructor.
+ EXPECT_CALL(mock_js_backend_,
+ AddHandler(GetTestSyncInternalsUIAddress()));
+
+ // Called by SyncInternalUI's destructor.
+ EXPECT_CALL(mock_js_backend_,
+ RemoveHandler(GetTestSyncInternalsUIAddress()));
+ }
+
+ // Like ExpectSetupTeardownCalls() but with a NULL
+ // ProfileSyncService.
+ void ExpectSetupTeardownCallsNullService() {
+ EXPECT_CALL(profile_mock_, GetProfileSyncService())
+ .WillRepeatedly(Return(static_cast<ProfileSyncService*>(NULL)));
+ }
+
+ void ConstructTestSyncInternalsUI() {
+ if (test_sync_internals_ui_constructor_called_) {
+ ADD_FAILURE() << "ConstructTestSyncInternalsUI() should be called "
+ << "at most once per test";
+ return;
+ }
+ new(test_sync_internals_ui_buf_) TestSyncInternalsUI(&test_tab_contents_);
+ test_sync_internals_ui_constructor_called_ = true;
+ }
+
+ TestSyncInternalsUI* GetTestSyncInternalsUI() {
+ if (!test_sync_internals_ui_constructor_called_) {
+ ADD_FAILURE() << "ConstructTestSyncInternalsUI() should be called "
+ << "before GetTestSyncInternalsUI()";
+ return NULL;
+ }
+ return GetTestSyncInternalsUIAddress();
+ }
+
+ // Used for passing into EXPECT_CALL().
+ TestSyncInternalsUI* GetTestSyncInternalsUIAddress() {
+ return static_cast<TestSyncInternalsUI*>(test_sync_internals_ui_buf_);
+ }
+
+ NiceMock<ProfileMock> profile_mock_;
+ StrictMock<ProfileSyncServiceMock> profile_sync_service_mock_;
+ StrictMock<browser_sync::MockJsFrontend> mock_js_backend_;
+
+ private:
+ // Needed by |ui_thread_|.
+ MessageLoopForUI ui_loop_;
+ // Needed by |test_tab_contents_|.
+ BrowserThread ui_thread_;
+ TestTabContents test_tab_contents_;
+ void* test_sync_internals_ui_buf_;
+ bool test_sync_internals_ui_constructor_called_;
+};
+
+TEST_F(SyncInternalsUITest, HandleJsEvent) {
+ ExpectSetupTeardownCalls();
+
+ ConstructTestSyncInternalsUI();
+
+ EXPECT_CALL(*GetTestSyncInternalsUI(),
+ ExecuteJavascript(std::wstring(L"testMessage(5,true);")));
+
+ ListValue args;
+ args.Append(Value::CreateIntegerValue(5));
+ args.Append(Value::CreateBooleanValue(true));
+ GetTestSyncInternalsUI()->HandleJsEvent("testMessage", JsArgList(args));
+}
+
+TEST_F(SyncInternalsUITest, HandleJsEventNullService) {
+ ExpectSetupTeardownCallsNullService();
+
+ ConstructTestSyncInternalsUI();
+
+ EXPECT_CALL(*GetTestSyncInternalsUI(),
+ ExecuteJavascript(std::wstring(L"testMessage(5,true);")));
+
+ ListValue args;
+ args.Append(Value::CreateIntegerValue(5));
+ args.Append(Value::CreateBooleanValue(true));
+ GetTestSyncInternalsUI()->HandleJsEvent("testMessage", JsArgList(args));
+}
+
+TEST_F(SyncInternalsUITest, ProcessDOMUIMessageBasic) {
+ ExpectSetupTeardownCalls();
+
+ ViewHostMsg_DomMessage_Params params;
+ params.name = "testName";
+ params.arguments.Append(Value::CreateIntegerValue(10));
+
+ EXPECT_CALL(mock_js_backend_,
+ ProcessMessage(params.name, HasArgsAsList(params.arguments),
+ GetTestSyncInternalsUIAddress()));
+
+ ConstructTestSyncInternalsUI();
+
+ GetTestSyncInternalsUI()->ProcessDOMUIMessage(params);
+}
+
+TEST_F(SyncInternalsUITest, ProcessDOMUIMessageBasicNullService) {
+ ExpectSetupTeardownCallsNullService();
+
+ ConstructTestSyncInternalsUI();
+
+ ViewHostMsg_DomMessage_Params params;
+ params.name = "testName";
+ params.arguments.Append(Value::CreateIntegerValue(5));
+
+ // Should drop the message.
+ GetTestSyncInternalsUI()->ProcessDOMUIMessage(params);
+}
+
+namespace {
+const wchar_t kAboutInfoCall[] =
+ L"onGetAboutInfoFinished({\"summary\":\"SYNC DISABLED\"});";
+} // namespace
+
+TEST_F(SyncInternalsUITest, ProcessDOMUIMessageGetAboutInfo) {
+ ExpectSetupTeardownCalls();
+
+ ViewHostMsg_DomMessage_Params params;
+ params.name = "getAboutInfo";
+
+ ConstructTestSyncInternalsUI();
+
+ EXPECT_CALL(*GetTestSyncInternalsUI(),
+ ExecuteJavascript(std::wstring(kAboutInfoCall)));
+
+ GetTestSyncInternalsUI()->ProcessDOMUIMessage(params);
+}
+
+TEST_F(SyncInternalsUITest, ProcessDOMUIMessageGetAboutInfoNullService) {
+ ExpectSetupTeardownCallsNullService();
+
+ ViewHostMsg_DomMessage_Params params;
+ params.name = "getAboutInfo";
+
+ ConstructTestSyncInternalsUI();
+
+ EXPECT_CALL(*GetTestSyncInternalsUI(),
+ ExecuteJavascript(std::wstring(kAboutInfoCall)));
+
+ GetTestSyncInternalsUI()->ProcessDOMUIMessage(params);
+}
+
+} // namespace
diff --git a/chrome/browser/resources/sync_internals/sync_index.html b/chrome/browser/resources/sync_internals/sync_index.html
index 26ed19e..51e397d 100644
--- a/chrome/browser/resources/sync_internals/sync_index.html
+++ b/chrome/browser/resources/sync_internals/sync_index.html
@@ -7,6 +7,7 @@ chrome/test/functional/special_tabs.py. -->
<script>
function onLoad() {
chrome.send('getAboutInfo');
+ chrome.send('getNotificationState');
}
function onGetAboutInfoFinished(aboutInfo) {
@@ -14,9 +15,42 @@ function onGetAboutInfoFinished(aboutInfo) {
jstProcess(new JsEvalContext(aboutInfo), aboutInfoDiv);
}
+function onGetNotificationStateFinished(notificationsEnabled) {
+ onSyncNotificationStateChange(notificationsEnabled);
+}
+
function onSyncServiceStateChanged() {
chrome.send('getAboutInfo');
}
+
+function onSyncNotificationStateChange(notificationsEnabled) {
+ var notificationsEnabledInfo =
+ document.getElementById('notificationsEnabledInfo');
+ jstProcess(
+ new JsEvalContext({ 'notificationsEnabled': notificationsEnabled }),
+ notificationsEnabledInfo);
+}
+
+var notificationCounts = {};
+
+function onSyncIncomingNotification(changedTypes) {
+ for (var i = 0; i < changedTypes.length; ++i) {
+ var changedType = changedTypes[i];
+ notificationCounts[changedType] = notificationCounts[changedType] || 0;
+ ++notificationCounts[changedType];
+ }
+
+ var infos = [];
+ for (var k in notificationCounts) {
+ var info = { 'modelType': k, 'notificationCount': notificationCounts[k] };
+ infos.push(info);
+ }
+
+ var notificationCountsInfo =
+ document.getElementById('notificationCountsInfo');
+ jstProcess(new JsEvalContext({ 'notificationCounts': infos }),
+ notificationCountsInfo);
+}
</script>
<style type="text/css">
@@ -223,5 +257,22 @@ table.list#details .name {
</div>
</div>
+<div class="desc"><h2> Notifications </h2></div>
+<p id='notificationsEnabledInfo'>
+ Enabled: <span jscontent='notificationsEnabled'></span>
+</p>
+
+<table class='list' id='notificationCountsInfo'>
+ <tr jsselect='notificationCounts'>
+ <td class='name'>
+ <div jscontent='modelType'></div>
+ </td>
+ <td class='number'>
+ <div jscontent='notificationCount'></div>
+ </td>
+ </tr>
+</table>
+</td>
+
</body>
</html>
diff --git a/chrome/browser/sync/README.js b/chrome/browser/sync/README.js
new file mode 100644
index 0000000..d902b71
--- /dev/null
+++ b/chrome/browser/sync/README.js
@@ -0,0 +1,54 @@
+Overview of chrome://sync-internals
+-----------------------------------
+
+This note explains how chrome://sync-internals (also known as
+about:sync) interacts with the sync service/backend.
+
+Basically, chrome://sync-internals sends asynchronous messages to the
+sync backend and the sync backend asynchronously raises events to
+chrome://sync-internals, either when replying to messages or when
+something interesting happens.
+
+Both messages and events have a name and a list of arguments, the
+latter of which is represented by a JsArgList (js_arg_list.h) object,
+which is basically a wrapper around an immutable ListValue.
+
+Message/event flow
+------------------
+
+chrome://sync-internals is represented by SyncInternalsUI
+(chrome/browser/dom_ui/sync_internals_ui.h). SyncInternalsUI
+interacts with the sync service via a JsFrontend (js_frontend.h)
+object, which has a ProcessMessage() method. The JsFrontend can
+handle some messages itself, but it can also delegate the rest to a
+JsBackend instance (js_backend.h), which also has a ProcessMessage()
+method. A JsBackend can in turn handle some messages itself and
+delegate to other JsBackend instances.
+
+Essentially, there is a tree with a JsFrontend as the root and
+JsBackend as non-root internal nodes and leaf nodes (although
+currently, the tree is more like a simple list). The sets of messages
+handled by the JsBackends and the JsFrontend are disjoint, which means
+that at most one node handles a given message type. Also, the
+JsBackends may live on different threads, but JsArgList is thread-safe
+so that's okay.
+
+SyncInternalsUI is a JsEventHandler (js_event_handler.h), which means
+that it has a HandleJsEvent() method, but JsBackends cannot easily
+access those objects. Instead, each JsBackend keeps track of its
+parent router, which is a JsEventRouter object (js_event_router.h).
+Basically, a JsEventRouter is another JsBackend object or a JsFrontend
+object. So an event travels up through the JsEventRouter until it
+reaches the JsFrontend, which knows about the existing JsEventHandlers
+(via AddHandler()/RemoveHandler()) and so can delegate to the right
+one.
+
+A diagram of the flow of a message and its reply:
+
+msg(args) -> F -> B -> B -> B
+ | | |
+ H <- R <- R <- R <- reply-event(args)
+
+F = JsFrontend, B = JsBackend, R = JsEventRouter, H = JsEventHandler
+
+Non-reply events are percolated up similarly.
diff --git a/chrome/browser/sync/engine/syncapi.cc b/chrome/browser/sync/engine/syncapi.cc
index a275ae9..f989da1 100644
--- a/chrome/browser/sync/engine/syncapi.cc
+++ b/chrome/browser/sync/engine/syncapi.cc
@@ -19,6 +19,7 @@
#include "base/synchronization/lock.h"
#include "base/task.h"
#include "base/utf_string_conversions.h"
+#include "base/values.h"
#include "chrome/browser/browser_thread.h"
#include "chrome/browser/sync/sync_constants.h"
#include "chrome/browser/sync/engine/all_status.h"
@@ -28,6 +29,9 @@
#include "chrome/browser/sync/engine/net/syncapi_server_connection_manager.h"
#include "chrome/browser/sync/engine/syncer.h"
#include "chrome/browser/sync/engine/syncer_thread.h"
+#include "chrome/browser/sync/js_arg_list.h"
+#include "chrome/browser/sync/js_backend.h"
+#include "chrome/browser/sync/js_event_router.h"
#include "chrome/browser/sync/notifier/server_notifier_thread.h"
#include "chrome/browser/sync/notifier/state_writer.h"
#include "chrome/browser/sync/protocol/app_specifics.pb.h"
@@ -957,6 +961,7 @@ class SyncManager::SyncInternal
public TalkMediator::Delegate,
public sync_notifier::StateWriter,
public browser_sync::ChannelEventHandler<syncable::DirectoryChangeEvent>,
+ public browser_sync::JsBackend,
public SyncEngineEventListener {
static const int kDefaultNudgeDelayMilliseconds;
static const int kPreferencesNudgeDelayMilliseconds;
@@ -964,6 +969,7 @@ class SyncManager::SyncInternal
explicit SyncInternal(SyncManager* sync_manager)
: core_message_loop_(NULL),
observer_(NULL),
+ parent_router_(NULL),
sync_manager_(sync_manager),
registrar_(NULL),
notification_pending_(false),
@@ -1150,6 +1156,15 @@ class SyncManager::SyncInternal
// SyncEngineEventListener implementation.
virtual void OnSyncEngineEvent(const SyncEngineEvent& event);
+
+ // browser_sync::JsBackend implementation.
+ virtual void SetParentJsEventRouter(browser_sync::JsEventRouter* router);
+ virtual void RemoveParentJsEventRouter();
+ virtual const browser_sync::JsEventRouter* GetParentJsEventRouter() const;
+ virtual void ProcessMessage(const std::string& name,
+ const browser_sync::JsArgList& args,
+ const browser_sync::JsEventHandler* sender);
+
private:
// Helper to handle the details of initializing the TalkMediator.
// Must be called only after OpenDirectory() is called.
@@ -1259,6 +1274,8 @@ class SyncManager::SyncInternal
// WARNING: This can be NULL!
SyncManager::Observer* observer_;
+ browser_sync::JsEventRouter* parent_router_;
+
// The ServerConnectionManager used to abstract communication between the
// client (the Syncer) and the sync server.
scoped_ptr<SyncAPIServerConnectionManager> connection_manager_;
@@ -1782,6 +1799,10 @@ void SyncManager::RemoveObserver() {
data_->set_observer(NULL);
}
+browser_sync::JsBackend* SyncManager::GetJsBackend() {
+ return data_;
+}
+
void SyncManager::Shutdown() {
data_->Shutdown();
}
@@ -2154,6 +2175,42 @@ void SyncManager::SyncInternal::OnSyncEngineEvent(
}
}
+void SyncManager::SyncInternal::SetParentJsEventRouter(
+ browser_sync::JsEventRouter* router) {
+ DCHECK(router);
+ parent_router_ = router;
+}
+
+void SyncManager::SyncInternal::RemoveParentJsEventRouter() {
+ parent_router_ = NULL;
+}
+
+const browser_sync::JsEventRouter*
+ SyncManager::SyncInternal::GetParentJsEventRouter() const {
+ return parent_router_;
+}
+
+void SyncManager::SyncInternal::ProcessMessage(
+ const std::string& name, const browser_sync::JsArgList& args,
+ const browser_sync::JsEventHandler* sender) {
+ if (name == "getNotificationState") {
+ if (parent_router_) {
+ bool notifications_enabled = allstatus_.status().notifications_enabled;
+ ListValue return_args;
+ return_args.Append(Value::CreateBooleanValue(notifications_enabled));
+ parent_router_->RouteJsEvent(
+ "onGetNotificationStateFinished",
+ browser_sync::JsArgList(return_args), sender);
+ } else {
+ VLOG(1) << "No parent router; not replying to message " << name
+ << " with args " << args.ToString();
+ }
+ } else {
+ VLOG(1) << "Dropping unknown message " << name
+ << " with args " << args.ToString();
+ }
+}
+
void SyncManager::SyncInternal::OnNotificationStateChange(
bool notifications_enabled) {
VLOG(1) << "P2P: Notifications enabled = "
@@ -2162,6 +2219,13 @@ void SyncManager::SyncInternal::OnNotificationStateChange(
if (syncer_thread()) {
syncer_thread()->SetNotificationsEnabled(notifications_enabled);
}
+ if (parent_router_) {
+ ListValue args;
+ args.Append(Value::CreateBooleanValue(notifications_enabled));
+ // TODO(akalin): Tidy up grammar in event names.
+ parent_router_->RouteJsEvent("onSyncNotificationStateChange",
+ browser_sync::JsArgList(args), NULL);
+ }
if ((notifier_options_.notification_method !=
notifier::NOTIFICATION_SERVER) && notifications_enabled) {
// Nudge the syncer thread when notifications are enabled, in case there is
@@ -2248,6 +2312,21 @@ void SyncManager::SyncInternal::OnIncomingNotification(
} else {
LOG(WARNING) << "Sync received notification without any type information.";
}
+
+ if (parent_router_) {
+ ListValue args;
+ ListValue* changed_types = new ListValue();
+ args.Append(changed_types);
+ for (browser_sync::sessions::TypePayloadMap::const_iterator
+ it = model_types_with_payloads.begin();
+ it != model_types_with_payloads.end(); ++it) {
+ const std::string& model_type_str =
+ syncable::ModelTypeToString(it->first);
+ changed_types->Append(Value::CreateStringValue(model_type_str));
+ }
+ parent_router_->RouteJsEvent("onSyncIncomingNotification",
+ browser_sync::JsArgList(args), NULL);
+ }
}
void SyncManager::SyncInternal::OnOutgoingNotification() {
@@ -2321,4 +2400,17 @@ bool SyncManager::HasUnsyncedItems() const {
return (trans.GetWrappedTrans()->directory()->unsynced_entity_count() != 0);
}
+void SyncManager::TriggerOnNotificationStateChangeForTest(
+ bool notifications_enabled) {
+ data_->OnNotificationStateChange(notifications_enabled);
+}
+
+void SyncManager::TriggerOnIncomingNotificationForTest(
+ const syncable::ModelTypeBitSet& model_types) {
+ IncomingNotificationData notification_data;
+ notification_data.service_url = model_types.to_string();
+ // Here we rely on the default notification method being SERVER.
+ data_->OnIncomingNotification(notification_data);
+}
+
} // namespace sync_api
diff --git a/chrome/browser/sync/engine/syncapi.h b/chrome/browser/sync/engine/syncapi.h
index 601e58e..e215bda 100644
--- a/chrome/browser/sync/engine/syncapi.h
+++ b/chrome/browser/sync/engine/syncapi.h
@@ -56,6 +56,7 @@
class FilePath;
namespace browser_sync {
+class JsBackend;
class ModelSafeWorkerRegistrar;
namespace sessions {
@@ -899,6 +900,28 @@ class SyncManager {
// destroyed so the SyncManager doesn't potentially dereference garbage.
void RemoveObserver();
+ // Returns a pointer to the JsBackend (which is owned by the sync
+ // manager). Never returns NULL. The following events are sent by
+ // the returned backend:
+ //
+ // onSyncNotificationStateChange(boolean notificationsEnabled):
+ // Sent when notifications are enabled or disabled.
+ //
+ // onSyncIncomingNotification(array changedTypes):
+ // Sent when an incoming notification arrives. |changedTypes|
+ // contains a list of sync types (strings) which have changed.
+ //
+ // The following messages are processed by the returned backend:
+ //
+ // getNotificationState():
+ // If there is a parent router, sends the
+ // onGetNotificationStateFinished(boolean notifications_enabled)
+ // event to |sender| via the parent router with whether or not
+ // notifications are enabled.
+ //
+ // All other messages are dropped.
+ browser_sync::JsBackend* GetJsBackend();
+
// Status-related getters. Typically GetStatusSummary will suffice, but
// GetDetailedSyncStatus can be useful for gathering debug-level details of
// the internals of the sync engine.
@@ -925,6 +948,14 @@ class SyncManager {
// any remaining unsynced items.
bool HasUnsyncedItems() const;
+ // Functions used for testing.
+
+ void TriggerOnNotificationStateChangeForTest(
+ bool notifications_enabled);
+
+ void TriggerOnIncomingNotificationForTest(
+ const syncable::ModelTypeBitSet& model_types);
+
private:
// An opaque pointer to the nested private class.
SyncInternal* data_;
diff --git a/chrome/browser/sync/engine/syncapi_unittest.cc b/chrome/browser/sync/engine/syncapi_unittest.cc
index b83c501..3856ef8 100644
--- a/chrome/browser/sync/engine/syncapi_unittest.cc
+++ b/chrome/browser/sync/engine/syncapi_unittest.cc
@@ -6,17 +6,31 @@
// functionality is provided by the Syncable layer, which has its own
// unit tests. We'll test SyncApi specific things in this harness.
+#include "base/message_loop.h"
#include "base/scoped_ptr.h"
#include "base/scoped_temp_dir.h"
+#include "base/values.h"
+#include "chrome/browser/browser_thread.h"
#include "chrome/browser/sync/engine/syncapi.h"
+#include "chrome/browser/sync/js_arg_list.h"
+#include "chrome/browser/sync/js_backend.h"
+#include "chrome/browser/sync/js_event_handler.h"
+#include "chrome/browser/sync/js_event_router.h"
+#include "chrome/browser/sync/js_test_util.h"
#include "chrome/browser/sync/protocol/password_specifics.pb.h"
#include "chrome/browser/sync/syncable/directory_manager.h"
#include "chrome/browser/sync/syncable/syncable.h"
#include "chrome/test/sync/engine/test_directory_setter_upper.h"
-
+#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
+using browser_sync::HasArgsAsList;
using browser_sync::KeyParams;
+using browser_sync::JsArgList;
+using browser_sync::MockJsEventHandler;
+using browser_sync::MockJsEventRouter;
+using testing::_;
+using testing::StrictMock;
namespace sync_api {
@@ -264,4 +278,153 @@ TEST_F(SyncApiTest, WriteAndReadPassword) {
}
}
+namespace {
+
+class SyncManagerTest : public testing::Test {
+ protected:
+ SyncManagerTest() : ui_thread_(BrowserThread::UI, &ui_loop_) {}
+
+ private:
+ // Needed by |ui_thread_|.
+ MessageLoopForUI ui_loop_;
+ // Needed by |sync_manager_|.
+ BrowserThread ui_thread_;
+
+ protected:
+ SyncManager sync_manager_;
+};
+
+TEST_F(SyncManagerTest, ParentJsEventRouter) {
+ StrictMock<MockJsEventRouter> event_router;
+ browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();
+ EXPECT_EQ(NULL, js_backend->GetParentJsEventRouter());
+ js_backend->SetParentJsEventRouter(&event_router);
+ EXPECT_EQ(&event_router, js_backend->GetParentJsEventRouter());
+ js_backend->RemoveParentJsEventRouter();
+ EXPECT_EQ(NULL, js_backend->GetParentJsEventRouter());
+}
+
+TEST_F(SyncManagerTest, ProcessMessage) {
+ const JsArgList kNoArgs;
+
+ browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();
+
+ // Messages sent without any parent router should be dropped.
+ {
+ StrictMock<MockJsEventHandler> event_handler;
+ js_backend->ProcessMessage("unknownMessage",
+ kNoArgs, &event_handler);
+ js_backend->ProcessMessage("getNotificationState",
+ kNoArgs, &event_handler);
+ }
+
+ {
+ StrictMock<MockJsEventHandler> event_handler;
+ StrictMock<MockJsEventRouter> event_router;
+
+ ListValue false_args;
+ false_args.Append(Value::CreateBooleanValue(false));
+
+ EXPECT_CALL(event_router,
+ RouteJsEvent("onGetNotificationStateFinished",
+ HasArgsAsList(false_args),
+ &event_handler)).Times(1);
+
+ js_backend->SetParentJsEventRouter(&event_router);
+
+ // This message should be dropped.
+ js_backend->ProcessMessage("unknownMessage",
+ kNoArgs, &event_handler);
+
+ // This should trigger the reply.
+ js_backend->ProcessMessage("getNotificationState",
+ kNoArgs, &event_handler);
+
+ js_backend->RemoveParentJsEventRouter();
+ }
+
+ // Messages sent after a parent router has been removed should be
+ // dropped.
+ {
+ StrictMock<MockJsEventHandler> event_handler;
+ js_backend->ProcessMessage("unknownMessage",
+ kNoArgs, &event_handler);
+ js_backend->ProcessMessage("getNotificationState",
+ kNoArgs, &event_handler);
+ }
+}
+
+TEST_F(SyncManagerTest, OnNotificationStateChange) {
+ StrictMock<MockJsEventRouter> event_router;
+
+ ListValue true_args;
+ true_args.Append(Value::CreateBooleanValue(true));
+ ListValue false_args;
+ false_args.Append(Value::CreateBooleanValue(false));
+
+ EXPECT_CALL(event_router,
+ RouteJsEvent("onSyncNotificationStateChange",
+ HasArgsAsList(true_args), NULL)).Times(1);
+ EXPECT_CALL(event_router,
+ RouteJsEvent("onSyncNotificationStateChange",
+ HasArgsAsList(false_args), NULL)).Times(1);
+
+ browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();
+
+ sync_manager_.TriggerOnNotificationStateChangeForTest(true);
+ sync_manager_.TriggerOnNotificationStateChangeForTest(false);
+
+ js_backend->SetParentJsEventRouter(&event_router);
+ sync_manager_.TriggerOnNotificationStateChangeForTest(true);
+ sync_manager_.TriggerOnNotificationStateChangeForTest(false);
+ js_backend->RemoveParentJsEventRouter();
+
+ sync_manager_.TriggerOnNotificationStateChangeForTest(true);
+ sync_manager_.TriggerOnNotificationStateChangeForTest(false);
+}
+
+TEST_F(SyncManagerTest, OnIncomingNotification) {
+ StrictMock<MockJsEventRouter> event_router;
+
+ const syncable::ModelTypeBitSet empty_model_types;
+ syncable::ModelTypeBitSet model_types;
+ model_types.set(syncable::BOOKMARKS);
+ model_types.set(syncable::THEMES);
+
+ // Build expected_args to have a single argument with the string
+ // equivalents of model_types.
+ ListValue expected_args;
+ {
+ ListValue* model_type_list = new ListValue();
+ expected_args.Append(model_type_list);
+ for (int i = syncable::FIRST_REAL_MODEL_TYPE;
+ i < syncable::MODEL_TYPE_COUNT; ++i) {
+ if (model_types[i]) {
+ model_type_list->Append(
+ Value::CreateStringValue(
+ syncable::ModelTypeToString(
+ syncable::ModelTypeFromInt(i))));
+ }
+ }
+ }
+
+ EXPECT_CALL(event_router,
+ RouteJsEvent("onSyncIncomingNotification",
+ HasArgsAsList(expected_args), NULL)).Times(1);
+
+ browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();
+
+ sync_manager_.TriggerOnIncomingNotificationForTest(empty_model_types);
+ sync_manager_.TriggerOnIncomingNotificationForTest(model_types);
+
+ js_backend->SetParentJsEventRouter(&event_router);
+ sync_manager_.TriggerOnIncomingNotificationForTest(model_types);
+ js_backend->RemoveParentJsEventRouter();
+
+ sync_manager_.TriggerOnIncomingNotificationForTest(empty_model_types);
+ sync_manager_.TriggerOnIncomingNotificationForTest(model_types);
+}
+
+} // namespace
+
} // namespace browser_sync
diff --git a/chrome/browser/sync/glue/sync_backend_host.cc b/chrome/browser/sync/glue/sync_backend_host.cc
index 9b154fd..9230c2c 100644
--- a/chrome/browser/sync/glue/sync_backend_host.cc
+++ b/chrome/browser/sync/glue/sync_backend_host.cc
@@ -25,6 +25,7 @@
#include "chrome/browser/sync/glue/sync_backend_host.h"
#include "chrome/browser/sync/glue/http_bridge.h"
#include "chrome/browser/sync/glue/password_model_worker.h"
+#include "chrome/browser/sync/js_arg_list.h"
#include "chrome/browser/sync/sessions/session_state.h"
// TODO(tim): Remove this! We should have a syncapi pass-thru instead.
#include "chrome/browser/sync/syncable/directory_manager.h" // Cryptographer.
@@ -179,6 +180,15 @@ bool SyncBackendHost::IsCryptographerReady() const {
GetUserShare()->dir_manager->cryptographer()->is_ready();
}
+JsBackend* SyncBackendHost::GetJsBackend() {
+ if (syncapi_initialized_) {
+ return core_.get();
+ } else {
+ NOTREACHED();
+ return NULL;
+ }
+}
+
sync_api::HttpPostProviderFactory* SyncBackendHost::MakeHttpBridgeFactory(
URLRequestContextGetter* getter) {
return new HttpBridgeFactory(getter);
@@ -560,7 +570,8 @@ bool SyncBackendHost::HasUnsyncedItems() const {
SyncBackendHost::Core::Core(SyncBackendHost* backend)
: host_(backend),
- syncapi_(new sync_api::SyncManager()) {
+ syncapi_(new sync_api::SyncManager()),
+ parent_router_(NULL) {
}
// Helper to construct a user agent string (ASCII) suitable for use by
@@ -656,6 +667,7 @@ void SyncBackendHost::Core::DoShutdown(bool sync_disabled) {
save_changes_timer_.Stop();
syncapi_->Shutdown(); // Stops the SyncerThread.
syncapi_->RemoveObserver();
+ DisconnectChildJsEventRouter();
host_->ui_worker()->OnSyncerShutdownComplete();
if (sync_disabled)
@@ -808,7 +820,6 @@ bool SyncBackendHost::Core::IsCurrentThreadSafeForModel(
return worker->CurrentThreadIsWorkThread();
}
-
void SyncBackendHost::Core::OnAuthError(const AuthError& auth_error) {
// Post to our core loop so we can modify state. Could be on another thread.
host_->frontend_loop_->PostTask(FROM_HERE,
@@ -860,6 +871,14 @@ void SyncBackendHost::Core::OnClearServerDataFailed() {
&Core::HandleClearServerDataFailedOnFrontendLoop));
}
+void SyncBackendHost::Core::RouteJsEvent(
+ const std::string& name, const JsArgList& args,
+ const JsEventHandler* target) {
+ host_->frontend_loop_->PostTask(
+ FROM_HERE, NewRunnableMethod(
+ this, &Core::RouteJsEventOnFrontendLoop, name, args, target));
+}
+
void SyncBackendHost::Core::HandleStopSyncingPermanentlyOnFrontendLoop() {
if (!host_ || !host_->frontend_)
return;
@@ -889,6 +908,17 @@ void SyncBackendHost::Core::HandleAuthErrorEventOnFrontendLoop(
host_->frontend_->OnAuthError();
}
+void SyncBackendHost::Core::RouteJsEventOnFrontendLoop(
+ const std::string& name, const JsArgList& args,
+ const JsEventHandler* target) {
+ if (!host_ || !parent_router_)
+ return;
+
+ DCHECK_EQ(MessageLoop::current(), host_->frontend_loop_);
+
+ parent_router_->RouteJsEvent(name, args, target);
+}
+
void SyncBackendHost::Core::StartSavingChanges() {
save_changes_timer_.Start(
base::TimeDelta::FromSeconds(kSaveChangesIntervalSeconds),
@@ -922,4 +952,62 @@ void SyncBackendHost::Core::DeleteSyncDataFolder() {
}
}
+void SyncBackendHost::Core::SetParentJsEventRouter(JsEventRouter* router) {
+ DCHECK_EQ(MessageLoop::current(), host_->frontend_loop_);
+ DCHECK(router);
+ parent_router_ = router;
+ MessageLoop* core_message_loop = host_->core_thread_.message_loop();
+ CHECK(core_message_loop);
+ core_message_loop->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(this,
+ &SyncBackendHost::Core::ConnectChildJsEventRouter));
+}
+
+void SyncBackendHost::Core::RemoveParentJsEventRouter() {
+ DCHECK_EQ(MessageLoop::current(), host_->frontend_loop_);
+ parent_router_ = NULL;
+ MessageLoop* core_message_loop = host_->core_thread_.message_loop();
+ CHECK(core_message_loop);
+ core_message_loop->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(this,
+ &SyncBackendHost::Core::DisconnectChildJsEventRouter));
+}
+
+const JsEventRouter* SyncBackendHost::Core::GetParentJsEventRouter() const {
+ DCHECK_EQ(MessageLoop::current(), host_->frontend_loop_);
+ return parent_router_;
+}
+
+void SyncBackendHost::Core::ProcessMessage(
+ const std::string& name, const JsArgList& args,
+ const JsEventHandler* sender) {
+ DCHECK_EQ(MessageLoop::current(), host_->frontend_loop_);
+ MessageLoop* core_message_loop = host_->core_thread_.message_loop();
+ CHECK(core_message_loop);
+ core_message_loop->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(this,
+ &SyncBackendHost::Core::DoProcessMessage,
+ name, args, sender));
+}
+
+void SyncBackendHost::Core::ConnectChildJsEventRouter() {
+ DCHECK_EQ(MessageLoop::current(), host_->core_thread_.message_loop());
+ syncapi_->GetJsBackend()->SetParentJsEventRouter(this);
+}
+
+void SyncBackendHost::Core::DisconnectChildJsEventRouter() {
+ DCHECK_EQ(MessageLoop::current(), host_->core_thread_.message_loop());
+ syncapi_->GetJsBackend()->RemoveParentJsEventRouter();
+}
+
+void SyncBackendHost::Core::DoProcessMessage(
+ const std::string& name, const JsArgList& args,
+ const JsEventHandler* sender) {
+ DCHECK_EQ(MessageLoop::current(), host_->core_thread_.message_loop());
+ syncapi_->GetJsBackend()->ProcessMessage(name, args, sender);
+}
+
} // namespace browser_sync
diff --git a/chrome/browser/sync/glue/sync_backend_host.h b/chrome/browser/sync/glue/sync_backend_host.h
index 01954a5..f7576493 100644
--- a/chrome/browser/sync/glue/sync_backend_host.h
+++ b/chrome/browser/sync/glue/sync_backend_host.h
@@ -19,8 +19,10 @@
#include "base/utf_string_conversions.h"
#include "chrome/browser/sync/engine/syncapi.h"
#include "chrome/browser/sync/engine/model_safe_worker.h"
+#include "chrome/browser/sync/js_backend.h"
#include "chrome/browser/sync/glue/data_type_controller.h"
#include "chrome/browser/sync/glue/ui_model_worker.h"
+#include "chrome/browser/sync/js_event_router.h"
#include "chrome/browser/sync/syncable/model_type.h"
#include "chrome/common/net/gaia/google_service_auth_error.h"
#include "chrome/common/net/url_request_context_getter.h"
@@ -42,6 +44,7 @@ struct SyncSessionSnapshot;
class ChangeProcessor;
class DataTypeController;
+class JsArgList;
// SyncFrontend is the interface used by SyncBackendHost to communicate with
// the entity that created it and, presumably, is interested in sync-related
@@ -110,7 +113,7 @@ class SyncBackendHost : public browser_sync::ModelSafeWorkerRegistrar {
// For testing.
// TODO(skrul): Extract an interface so this is not needed.
SyncBackendHost();
- ~SyncBackendHost();
+ virtual ~SyncBackendHost();
// Called on |frontend_loop_| to kick off asynchronous initialization.
// As a fallback when no cached auth information is available, try to
@@ -231,10 +234,21 @@ class SyncBackendHost : public browser_sync::ModelSafeWorkerRegistrar {
// using a token previously received.
bool IsCryptographerReady() const;
+ // Returns a pointer to the JsBackend (which is owned by the
+ // service). Must be called only after the sync backend has been
+ // initialized, and never returns NULL if you do so. Overrideable
+ // for testing purposes.
+ virtual JsBackend* GetJsBackend();
+
+ // TODO(akalin): Write unit tests for the JsBackend, finding a way
+ // to make this class testable in general.
+
protected:
// The real guts of SyncBackendHost, to keep the public client API clean.
class Core : public base::RefCountedThreadSafe<SyncBackendHost::Core>,
- public sync_api::SyncManager::Observer {
+ public sync_api::SyncManager::Observer,
+ public JsBackend,
+ public JsEventRouter {
public:
explicit Core(SyncBackendHost* backend);
@@ -260,6 +274,18 @@ class SyncBackendHost : public browser_sync::ModelSafeWorkerRegistrar {
virtual void OnClearServerDataFailed();
virtual void OnClearServerDataSucceeded();
+ // JsBackend implementation.
+ virtual void SetParentJsEventRouter(JsEventRouter* router);
+ virtual void RemoveParentJsEventRouter();
+ virtual const JsEventRouter* GetParentJsEventRouter() const;
+ virtual void ProcessMessage(const std::string& name, const JsArgList& args,
+ const JsEventHandler* sender);
+
+ // JsEventRouter implementation.
+ virtual void RouteJsEvent(const std::string& event_name,
+ const JsArgList& args,
+ const JsEventHandler* dst);
+
struct DoInitializeOptions {
DoInitializeOptions(
const GURL& service_url,
@@ -332,6 +358,14 @@ class SyncBackendHost : public browser_sync::ModelSafeWorkerRegistrar {
// sync databases), as well as shutdown when you're no longer syncing.
void DeleteSyncDataFolder();
+ void ConnectChildJsEventRouter();
+
+ void DisconnectChildJsEventRouter();
+
+ void DoProcessMessage(
+ const std::string& name, const JsArgList& args,
+ const JsEventHandler* sender);
+
#if defined(UNIT_TEST)
// Special form of initialization that does not try and authenticate the
// last known user (since it will fail in test mode) and does some extra
@@ -354,7 +388,7 @@ class SyncBackendHost : public browser_sync::ModelSafeWorkerRegistrar {
friend class base::RefCountedThreadSafe<SyncBackendHost::Core>;
friend class SyncBackendHostForProfileSyncTest;
- ~Core();
+ virtual ~Core();
// Return change processor for a particular model (return NULL on failure).
ChangeProcessor* GetProcessor(syncable::ModelType modeltype);
@@ -413,12 +447,13 @@ class SyncBackendHost : public browser_sync::ModelSafeWorkerRegistrar {
// frontend thread components.
void HandleInitalizationCompletedOnFrontendLoop();
+ void RouteJsEventOnFrontendLoop(
+ const std::string& name, const JsArgList& args,
+ const JsEventHandler* dst);
+
// Return true if a model lives on the current thread.
bool IsCurrentThreadSafeForModel(syncable::ModelType model_type);
- // True if credentials are ready for sync use.
- bool CredentialsAvailable();
-
// Our parent SyncBackendHost
SyncBackendHost* host_;
@@ -428,6 +463,8 @@ class SyncBackendHost : public browser_sync::ModelSafeWorkerRegistrar {
// The top-level syncapi entry point.
scoped_ptr<sync_api::SyncManager> syncapi_;
+ JsEventRouter* parent_router_;
+
DISALLOW_COPY_AND_ASSIGN(Core);
};
diff --git a/chrome/browser/sync/js_arg_list.cc b/chrome/browser/sync/js_arg_list.cc
new file mode 100644
index 0000000..538b07c
--- /dev/null
+++ b/chrome/browser/sync/js_arg_list.cc
@@ -0,0 +1,53 @@
+// Copyright (c) 2011 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 "chrome/browser/sync/js_arg_list.h"
+
+#include "base/json/json_writer.h"
+#include "base/scoped_ptr.h"
+
+namespace browser_sync {
+
+JsArgList::JsArgList() : args_(new SharedListValue()) {}
+
+JsArgList::JsArgList(const ListValue& args)
+ : args_(new SharedListValue(args)) {}
+
+JsArgList::JsArgList(const std::vector<const Value*>& args)
+ : args_(new SharedListValue(args)) {}
+
+const ListValue& JsArgList::Get() const {
+ return args_->Get();
+}
+
+std::string JsArgList::ToString() const {
+ std::string str;
+ base::JSONWriter::Write(&Get(), false, &str);
+ return str;
+}
+
+JsArgList::SharedListValue::SharedListValue() {}
+
+JsArgList::SharedListValue::SharedListValue(const ListValue& list_value) {
+ // Go through contortions to copy the list since ListValues are not
+ // copyable.
+ scoped_ptr<ListValue> list_value_copy(list_value.DeepCopy());
+ list_value_.Swap(list_value_copy.get());
+}
+
+JsArgList::SharedListValue::SharedListValue(
+ const std::vector<const Value*>& value_list) {
+ for (std::vector<const Value*>::const_iterator it = value_list.begin();
+ it != value_list.end(); ++it) {
+ list_value_.Append((*it)->DeepCopy());
+ }
+}
+
+const ListValue& JsArgList::SharedListValue::Get() const {
+ return list_value_;
+}
+
+JsArgList::SharedListValue::~SharedListValue() {}
+
+} // namespace browser_sync
diff --git a/chrome/browser/sync/js_arg_list.h b/chrome/browser/sync/js_arg_list.h
new file mode 100644
index 0000000..80ea55e
--- /dev/null
+++ b/chrome/browser/sync/js_arg_list.h
@@ -0,0 +1,54 @@
+// Copyright (c) 2011 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.
+
+#ifndef CHROME_BROWSER_SYNC_JS_ARG_LIST_H_
+#define CHROME_BROWSER_SYNC_JS_ARG_LIST_H_
+#pragma once
+
+// See README.js for design comments.
+
+#include <string>
+#include <vector>
+
+#include "base/ref_counted.h"
+#include "base/values.h"
+
+namespace browser_sync {
+
+// A thread-safe ref-counted wrapper around an immutable ListValue.
+// Used for passing around argument lists to different threads.
+class JsArgList {
+ public:
+ JsArgList();
+ explicit JsArgList(const ListValue& args);
+ explicit JsArgList(const std::vector<const Value*>& args);
+
+ const ListValue& Get() const;
+
+ std::string ToString() const;
+
+ private:
+ class SharedListValue : public base::RefCountedThreadSafe<SharedListValue> {
+ public:
+ SharedListValue();
+ explicit SharedListValue(const ListValue& list_value);
+ explicit SharedListValue(const std::vector<const Value*>& value_list);
+
+ const ListValue& Get() const;
+
+ private:
+ virtual ~SharedListValue();
+ friend class base::RefCountedThreadSafe<SharedListValue>;
+
+ ListValue list_value_;
+ };
+
+ scoped_refptr<const SharedListValue> args_;
+
+ // Copy constructor and assignment operator welcome.
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_JS_ARG_LIST_H_
diff --git a/chrome/browser/sync/js_arg_list_unittest.cc b/chrome/browser/sync/js_arg_list_unittest.cc
new file mode 100644
index 0000000..961a46b
--- /dev/null
+++ b/chrome/browser/sync/js_arg_list_unittest.cc
@@ -0,0 +1,63 @@
+// Copyright (c) 2011 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 "chrome/browser/sync/js_arg_list.h"
+
+#include "base/scoped_ptr.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace browser_sync {
+namespace {
+
+class JsArgListTest : public testing::Test {};
+
+TEST_F(JsArgListTest, EmptyList) {
+ JsArgList arg_list;
+ EXPECT_TRUE(arg_list.Get().empty());
+}
+
+TEST_F(JsArgListTest, FromList) {
+ scoped_ptr<ListValue> list(new ListValue());
+ list->Append(Value::CreateBooleanValue(false));
+ list->Append(Value::CreateIntegerValue(5));
+ DictionaryValue* dict = new DictionaryValue();
+ list->Append(dict);
+ dict->SetString("foo", "bar");
+ dict->Set("baz", new ListValue());
+
+ JsArgList arg_list(*list);
+
+ // Make sure arg_list takes a deep copy.
+ scoped_ptr<ListValue> list_copy(list->DeepCopy());
+ list.reset();
+ EXPECT_TRUE(arg_list.Get().Equals(list_copy.get()));
+}
+
+TEST_F(JsArgListTest, FromVector) {
+ FundamentalValue bool_value(false);
+ FundamentalValue int_value(5);
+ DictionaryValue dict;
+ dict.SetString("foo", "bar");
+ dict.Set("baz", new ListValue());
+
+ std::vector<const Value*> vec;
+ vec.push_back(&bool_value);
+ vec.push_back(&int_value);
+ vec.push_back(&dict);
+
+ JsArgList arg_list(vec);
+
+ ListValue list;
+ list.Append(bool_value.DeepCopy());
+ list.Append(int_value.DeepCopy());
+ list.Append(dict.DeepCopy());
+
+ // Make sure arg_list takes a deep copy.
+ vec.clear();
+ dict.SetString("baz", "foo");
+ EXPECT_TRUE(arg_list.Get().Equals(&list));
+}
+
+} // namespace
+} // namespace browser_sync
diff --git a/chrome/browser/sync/js_backend.h b/chrome/browser/sync/js_backend.h
new file mode 100644
index 0000000..6d5ec92
--- /dev/null
+++ b/chrome/browser/sync/js_backend.h
@@ -0,0 +1,44 @@
+// Copyright (c) 2011 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.
+
+#ifndef CHROME_BROWSER_SYNC_JS_BACKEND_H_
+#define CHROME_BROWSER_SYNC_JS_BACKEND_H_
+#pragma once
+
+// See README.js for design comments.
+
+#include <string>
+
+namespace browser_sync {
+
+class JsArgList;
+class JsEventHandler;
+class JsEventRouter;
+
+class JsBackend {
+ public:
+ // Sets the JS event router to which all backend events will be
+ // sent.
+ virtual void SetParentJsEventRouter(JsEventRouter* router) = 0;
+
+ // Removes any existing JS event router.
+ virtual void RemoveParentJsEventRouter() = 0;
+
+ // Gets the crurent JS event router, or NULL if there is none. Used
+ // for testing.
+ virtual const JsEventRouter* GetParentJsEventRouter() const = 0;
+
+ // Processes the given message. All reply events are sent to the
+ // parent JS event router (if set).
+ virtual void ProcessMessage(
+ const std::string& name, const JsArgList& args,
+ const JsEventHandler* sender) = 0;
+
+ protected:
+ virtual ~JsBackend() {}
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_JS_BACKEND_H_
diff --git a/chrome/browser/sync/js_event_handler.h b/chrome/browser/sync/js_event_handler.h
new file mode 100644
index 0000000..0e8b301
--- /dev/null
+++ b/chrome/browser/sync/js_event_handler.h
@@ -0,0 +1,30 @@
+// Copyright (c) 2011 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.
+
+#ifndef CHROME_BROWSER_SYNC_JS_EVENT_HANDLER_H_
+#define CHROME_BROWSER_SYNC_JS_EVENT_HANDLER_H_
+#pragma once
+
+// See README.js for design comments.
+
+#include <string>
+
+namespace browser_sync {
+
+class JsArgList;
+
+// An interface for objects that handle Javascript events (e.g.,
+// WebUIs).
+class JsEventHandler {
+ public:
+ virtual void HandleJsEvent(
+ const std::string& name, const JsArgList& args) = 0;
+
+ protected:
+ virtual ~JsEventHandler() {}
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_JS_EVENT_HANDLER_H_
diff --git a/chrome/browser/sync/js_event_handler_list.cc b/chrome/browser/sync/js_event_handler_list.cc
new file mode 100644
index 0000000..abc619385
--- /dev/null
+++ b/chrome/browser/sync/js_event_handler_list.cc
@@ -0,0 +1,94 @@
+// Copyright (c) 2011 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 "chrome/browser/sync/js_event_handler_list.h"
+
+#include <cstddef>
+
+#include "base/logging.h"
+#include "chrome/browser/sync/js_backend.h"
+#include "chrome/browser/sync/js_event_handler.h"
+
+namespace browser_sync {
+
+JsEventHandlerList::PendingMessage::PendingMessage(
+ const std::string& name, const JsArgList& args,
+ const JsEventHandler* sender)
+ : name(name), args(args), sender(sender) {}
+
+JsEventHandlerList::JsEventHandlerList() : backend_(NULL) {}
+
+JsEventHandlerList::~JsEventHandlerList() {
+ RemoveBackend();
+}
+
+// We connect to the backend only when necessary, i.e. when there is
+// at least one handler.
+
+void JsEventHandlerList::AddHandler(JsEventHandler* handler) {
+ handlers_.AddObserver(handler);
+ if (backend_) {
+ backend_->SetParentJsEventRouter(this);
+ }
+}
+
+void JsEventHandlerList::RemoveHandler(JsEventHandler* handler) {
+ handlers_.RemoveObserver(handler);
+ if (backend_ && handlers_.size() == 0) {
+ backend_->RemoveParentJsEventRouter();
+ }
+}
+
+void JsEventHandlerList::SetBackend(JsBackend* backend) {
+ DCHECK(!backend_);
+ DCHECK(backend);
+ backend_ = backend;
+
+ if (handlers_.size() > 0) {
+ backend_->SetParentJsEventRouter(this);
+
+ // Process any queued messages.
+ PendingMessageList pending_messages;
+ pending_messages_.swap(pending_messages);
+ for (PendingMessageList::const_iterator it = pending_messages.begin();
+ it != pending_messages.end(); ++it) {
+ backend_->ProcessMessage(it->name, it->args, it->sender);
+ }
+ }
+}
+
+void JsEventHandlerList::RemoveBackend() {
+ if (backend_) {
+ backend_->RemoveParentJsEventRouter();
+ backend_ = NULL;
+ }
+}
+
+void JsEventHandlerList::ProcessMessage(
+ const std::string& name, const JsArgList& args,
+ const JsEventHandler* sender) {
+ if (backend_) {
+ backend_->ProcessMessage(name, args, sender);
+ } else {
+ pending_messages_.push_back(PendingMessage(name, args, sender));
+ }
+}
+
+void JsEventHandlerList::RouteJsEvent(const std::string& name,
+ const JsArgList& args,
+ const JsEventHandler* target) {
+ if (target) {
+ JsEventHandler* non_const_target(const_cast<JsEventHandler*>(target));
+ if (handlers_.HasObserver(non_const_target)) {
+ non_const_target->HandleJsEvent(name, args);
+ } else {
+ VLOG(1) << "Unknown target; dropping event " << name
+ << " with args " << args.ToString();
+ }
+ } else {
+ FOR_EACH_OBSERVER(JsEventHandler, handlers_, HandleJsEvent(name, args));
+ }
+}
+
+} // namespace browser_sync
diff --git a/chrome/browser/sync/js_event_handler_list.h b/chrome/browser/sync/js_event_handler_list.h
new file mode 100644
index 0000000..2d5cf9e
--- /dev/null
+++ b/chrome/browser/sync/js_event_handler_list.h
@@ -0,0 +1,76 @@
+// Copyright (c) 2011 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.
+
+#ifndef CHROME_BROWSER_SYNC_JS_EVENT_HANDLER_LIST_H_
+#define CHROME_BROWSER_SYNC_JS_EVENT_HANDLER_LIST_H_
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/observer_list.h"
+#include "chrome/browser/sync/js_arg_list.h"
+#include "chrome/browser/sync/js_event_router.h"
+#include "chrome/browser/sync/js_frontend.h"
+
+namespace browser_sync {
+
+class JsBackend;
+class JsEventHandler;
+
+// A beefed-up ObserverList<JsEventHandler> that transparently handles
+// the communication between the handlers and a backend.
+class JsEventHandlerList : public JsFrontend, public JsEventRouter {
+ public:
+ JsEventHandlerList();
+
+ virtual ~JsEventHandlerList();
+
+ // Sets the backend to route all messages to. Should be called only
+ // if a backend has not already been set.
+ void SetBackend(JsBackend* backend);
+
+ // Removes any existing backend.
+ void RemoveBackend();
+
+ // JsFrontend implementation. Routes messages to any attached
+ // backend; if there is none, queues up the message for processing
+ // when the next backend is attached.
+ virtual void AddHandler(JsEventHandler* handler);
+ virtual void RemoveHandler(JsEventHandler* handler);
+ virtual void ProcessMessage(
+ const std::string& name, const JsArgList& args,
+ const JsEventHandler* sender);
+
+ // JsEventRouter implementation. Routes the event to the
+ // appropriate handler(s).
+ virtual void RouteJsEvent(const std::string& name,
+ const JsArgList& args,
+ const JsEventHandler* target);
+
+ private:
+ // A struct used to hold the arguments to ProcessMessage() for
+ // future invocation.
+ struct PendingMessage {
+ std::string name;
+ JsArgList args;
+ const JsEventHandler* sender;
+
+ PendingMessage(const std::string& name, const JsArgList& args,
+ const JsEventHandler* sender);
+ };
+
+ typedef std::vector<PendingMessage> PendingMessageList;
+
+ JsBackend* backend_;
+ ObserverList<JsEventHandler> handlers_;
+ PendingMessageList pending_messages_;
+
+ DISALLOW_COPY_AND_ASSIGN(JsEventHandlerList);
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_JS_EVENT_HANDLER_LIST_H_
diff --git a/chrome/browser/sync/js_event_handler_list_unittest.cc b/chrome/browser/sync/js_event_handler_list_unittest.cc
new file mode 100644
index 0000000..449e70f
--- /dev/null
+++ b/chrome/browser/sync/js_event_handler_list_unittest.cc
@@ -0,0 +1,114 @@
+// Copyright (c) 2011 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 "chrome/browser/sync/js_event_handler_list.h"
+
+#include "base/values.h"
+#include "chrome/browser/sync/js_arg_list.h"
+#include "chrome/browser/sync/js_test_util.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace browser_sync {
+namespace {
+
+using ::testing::StrictMock;
+
+class JsEventHandlerListTest : public testing::Test {};
+
+TEST_F(JsEventHandlerListTest, Basic) {
+ // |backend| must outlive |event_handlers|.
+ StrictMock<MockJsBackend> backend;
+
+ JsEventHandlerList event_handlers;
+
+ ListValue arg_list1, arg_list2;
+ arg_list1.Append(Value::CreateBooleanValue(false));
+ arg_list2.Append(Value::CreateIntegerValue(5));
+ JsArgList args1(arg_list1), args2(arg_list2);
+
+ StrictMock<MockJsEventHandler> handler1, handler2;
+
+ // Once from each call to AddHandler().
+ EXPECT_CALL(backend, SetParentJsEventRouter(&event_handlers)).Times(2);
+ // Once from the second RemoveHandler(), once from the destructor.
+ EXPECT_CALL(backend, RemoveParentJsEventRouter()).Times(2);
+ EXPECT_CALL(backend, ProcessMessage("test1", HasArgs(args2), &handler1));
+ EXPECT_CALL(backend, ProcessMessage("test2", HasArgs(args1), &handler2));
+
+ EXPECT_CALL(handler1, HandleJsEvent("reply1", HasArgs(args2)));
+ EXPECT_CALL(handler1, HandleJsEvent("allreply", HasArgs(args1)));
+
+ EXPECT_CALL(handler2, HandleJsEvent("reply2", HasArgs(args1)));
+ EXPECT_CALL(handler2, HandleJsEvent("allreply", HasArgs(args1)));
+ EXPECT_CALL(handler2, HandleJsEvent("anotherreply2", HasArgs(args2)));
+ EXPECT_CALL(handler2, HandleJsEvent("anotherallreply", HasArgs(args2)));
+
+ event_handlers.SetBackend(&backend);
+
+ event_handlers.AddHandler(&handler1);
+ event_handlers.AddHandler(&handler2);
+
+ event_handlers.ProcessMessage("test1", args2, &handler1);
+
+ event_handlers.RouteJsEvent("reply2", args1, &handler2);
+ event_handlers.RouteJsEvent("reply1", args2, &handler1);
+ event_handlers.RouteJsEvent("allreply", args1, NULL);
+
+ event_handlers.RemoveHandler(&handler1);
+
+ event_handlers.ProcessMessage("test2", args1, &handler2);
+
+ event_handlers.RouteJsEvent("droppedreply1", args1, &handler1);
+ event_handlers.RouteJsEvent("anotherreply2", args2, &handler2);
+ event_handlers.RouteJsEvent("anotherallreply", args2, NULL);
+
+ event_handlers.RemoveHandler(&handler2);
+
+ event_handlers.RouteJsEvent("anotherdroppedreply1", args1, &handler1);
+ event_handlers.RouteJsEvent("anotheranotherreply2", args2, &handler2);
+ event_handlers.RouteJsEvent("droppedallreply", args2, NULL);
+
+ // Let destructor of |event_handlers| call RemoveBackend().
+}
+
+TEST_F(JsEventHandlerListTest, QueuedMessages) {
+ // |backend| must outlive |event_handlers|.
+ StrictMock<MockJsBackend> backend;
+
+ JsEventHandlerList event_handlers;
+
+ ListValue arg_list1, arg_list2;
+ arg_list1.Append(Value::CreateBooleanValue(false));
+ arg_list2.Append(Value::CreateIntegerValue(5));
+ JsArgList args1(arg_list1), args2(arg_list2);
+
+ StrictMock<MockJsEventHandler> handler1, handler2;
+
+ // Once from the call to SetBackend().
+ EXPECT_CALL(backend, SetParentJsEventRouter(&event_handlers));
+ // Once from the first call to RemoveBackend().
+ EXPECT_CALL(backend, RemoveParentJsEventRouter());
+ // Once from the call to SetBackend().
+ EXPECT_CALL(backend, ProcessMessage("test1", HasArgs(args2), &handler1));
+ // Once from the call to SetBackend().
+ EXPECT_CALL(backend, ProcessMessage("test2", HasArgs(args1), &handler2));
+
+ event_handlers.AddHandler(&handler1);
+ event_handlers.AddHandler(&handler2);
+
+ // Should queue messages.
+ event_handlers.ProcessMessage("test1", args2, &handler1);
+ event_handlers.ProcessMessage("test2", args1, &handler2);
+
+ // Should call the queued messages.
+ event_handlers.SetBackend(&backend);
+
+ event_handlers.RemoveBackend();
+ // Should do nothing.
+ event_handlers.RemoveBackend();
+}
+
+} // namespace
+} // namespace browser_sync
diff --git a/chrome/browser/sync/js_event_router.h b/chrome/browser/sync/js_event_router.h
new file mode 100644
index 0000000..be783f4
--- /dev/null
+++ b/chrome/browser/sync/js_event_router.h
@@ -0,0 +1,37 @@
+// Copyright (c) 2011 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.
+
+#ifndef CHROME_BROWSER_SYNC_JS_EVENT_ROUTER_H_
+#define CHROME_BROWSER_SYNC_JS_EVENT_ROUTER_H_
+#pragma once
+
+// See README.js for design comments.
+
+#include <string>
+
+namespace browser_sync {
+
+class JsArgList;
+class JsEventHandler;
+
+// An interface for objects that don't directly handle Javascript
+// events but can pass them to JsEventHandlers or route them to other
+// JsEventRouters.
+class JsEventRouter {
+ public:
+ // If |target| is NULL that means the event is intended for every
+ // handler. Otherwise the event is meant for the given target only.
+ // |target| is const because it shouldn't be used except by the
+ // router that directly knows about it (which can then safely cast
+ // away the constness).
+ virtual void RouteJsEvent(const std::string& name, const JsArgList& args,
+ const JsEventHandler* target) = 0;
+
+ protected:
+ virtual ~JsEventRouter() {}
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_JS_EVENT_ROUTER_H_
diff --git a/chrome/browser/sync/js_frontend.h b/chrome/browser/sync/js_frontend.h
new file mode 100644
index 0000000..e57cacd
--- /dev/null
+++ b/chrome/browser/sync/js_frontend.h
@@ -0,0 +1,43 @@
+// Copyright (c) 2011 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.
+
+#ifndef CHROME_BROWSER_SYNC_JS_FRONTEND_H_
+#define CHROME_BROWSER_SYNC_JS_FRONTEND_H_
+#pragma once
+
+// See README.js for design comments.
+
+#include <string>
+
+namespace browser_sync {
+
+class JsArgList;
+class JsEventHandler;
+
+// An interface for objects that interact directly with
+// JsEventHandlers.
+class JsFrontend {
+ public:
+ // Adds a handler which will start receiving JS events (not
+ // immediately, so this can be called in the handler's constructor).
+ // A handler must be added at most once.
+ virtual void AddHandler(JsEventHandler* handler) = 0;
+
+ // Removes the given handler if it has been added. It will
+ // immediately stop receiving any JS events.
+ virtual void RemoveHandler(JsEventHandler* handler) = 0;
+
+ // Sends a JS message. The reply (if any) will be sent to |sender|
+ // if it has been added.
+ virtual void ProcessMessage(
+ const std::string& name, const JsArgList& args,
+ const JsEventHandler* sender) = 0;
+
+ protected:
+ virtual ~JsFrontend() {}
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_JS_FRONTEND_H_
diff --git a/chrome/browser/sync/js_test_util.cc b/chrome/browser/sync/js_test_util.cc
new file mode 100644
index 0000000..3c736c7
--- /dev/null
+++ b/chrome/browser/sync/js_test_util.cc
@@ -0,0 +1,60 @@
+// Copyright (c) 2011 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 "chrome/browser/sync/js_test_util.h"
+
+#include "base/basictypes.h"
+#include "chrome/browser/sync/js_arg_list.h"
+
+namespace browser_sync {
+
+void PrintTo(const JsArgList& args, ::std::ostream* os) {
+ *os << args.ToString();
+}
+
+namespace {
+
+// Matcher implementation for HasArgs().
+class HasArgsMatcher
+ : public ::testing::MatcherInterface<const JsArgList&> {
+ public:
+ explicit HasArgsMatcher(const JsArgList& expected_args)
+ : expected_args_(expected_args) {}
+
+ virtual ~HasArgsMatcher() {}
+
+ virtual bool MatchAndExplain(
+ const JsArgList& args,
+ ::testing::MatchResultListener* listener) const {
+ // No need to annotate listener since we already define PrintTo().
+ return args.Get().Equals(&expected_args_.Get());
+ }
+
+ virtual void DescribeTo(::std::ostream* os) const {
+ *os << "has args " << expected_args_.ToString();
+ }
+
+ virtual void DescribeNegationTo(::std::ostream* os) const {
+ *os << "doesn't have args " << expected_args_.ToString();
+ }
+
+ private:
+ const JsArgList expected_args_;
+
+ DISALLOW_COPY_AND_ASSIGN(HasArgsMatcher);
+};
+
+} // namespace
+
+::testing::Matcher<const JsArgList&> HasArgs(const JsArgList& expected_args) {
+ return ::testing::MakeMatcher(new HasArgsMatcher(expected_args));
+}
+
+::testing::Matcher<const JsArgList&> HasArgsAsList(
+ const ListValue& expected_args) {
+ return HasArgs(JsArgList(expected_args));
+}
+
+} // namespace browser_sync
+
diff --git a/chrome/browser/sync/js_test_util.h b/chrome/browser/sync/js_test_util.h
new file mode 100644
index 0000000..71badb3
--- /dev/null
+++ b/chrome/browser/sync/js_test_util.h
@@ -0,0 +1,70 @@
+// Copyright (c) 2011 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.
+
+#ifndef CHROME_BROWSER_SYNC_JS_TEST_UTIL_H_
+#define CHROME_BROWSER_SYNC_JS_TEST_UTIL_H_
+#pragma once
+
+#include <ostream>
+#include <string>
+
+#include "chrome/browser/sync/js_backend.h"
+#include "chrome/browser/sync/js_frontend.h"
+#include "chrome/browser/sync/js_event_handler.h"
+#include "chrome/browser/sync/js_event_router.h"
+#include "testing/gmock/include/gmock/gmock.h"
+
+class ListValue;
+
+namespace browser_sync {
+
+class JsArgList;
+
+// Defined for googletest. Equivalent to "*os << args.ToString()".
+void PrintTo(const JsArgList& args, ::std::ostream* os);
+
+// A matcher for gmock. Use like:
+//
+// EXPECT_CALL(mock, ProcessMessage("foo", HasArgs(args))).Times(1);
+::testing::Matcher<const JsArgList&> HasArgs(const JsArgList& expected_args);
+
+// Like HasArgs() but takes a ListValue instead.
+::testing::Matcher<const JsArgList&> HasArgsAsList(
+ const ListValue& expected_args);
+
+// Mocks.
+
+class MockJsBackend : public JsBackend {
+ public:
+ MOCK_METHOD1(SetParentJsEventRouter, void(JsEventRouter*));
+ MOCK_METHOD0(RemoveParentJsEventRouter, void());
+ MOCK_CONST_METHOD0(GetParentJsEventRouter, const JsEventRouter*());
+ MOCK_METHOD3(ProcessMessage, void(const ::std::string&, const JsArgList&,
+ const JsEventHandler*));
+};
+
+class MockJsFrontend : public JsFrontend {
+ public:
+ MOCK_METHOD1(AddHandler, void(JsEventHandler*));
+ MOCK_METHOD1(RemoveHandler, void(JsEventHandler*));
+ MOCK_METHOD3(ProcessMessage,
+ void(const ::std::string&, const JsArgList&,
+ const JsEventHandler*));
+};
+
+class MockJsEventHandler : public JsEventHandler {
+ public:
+ MOCK_METHOD2(HandleJsEvent, void(const ::std::string&, const JsArgList&));
+};
+
+class MockJsEventRouter : public JsEventRouter {
+ public:
+ MOCK_METHOD3(RouteJsEvent,
+ void(const ::std::string&, const JsArgList&,
+ const JsEventHandler*));
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_JS_TEST_UTIL_H_
diff --git a/chrome/browser/sync/profile_sync_service.cc b/chrome/browser/sync/profile_sync_service.cc
index 4ea3c79..bb25585 100644
--- a/chrome/browser/sync/profile_sync_service.cc
+++ b/chrome/browser/sync/profile_sync_service.cc
@@ -30,9 +30,9 @@
#include "chrome/browser/sync/glue/data_type_controller.h"
#include "chrome/browser/sync/glue/data_type_manager.h"
#include "chrome/browser/sync/glue/session_data_type_controller.h"
+#include "chrome/browser/sync/js_arg_list.h"
#include "chrome/browser/sync/profile_sync_factory.h"
#include "chrome/browser/sync/signin_manager.h"
-#include "chrome/browser/sync/sync_ui_util.h"
#include "chrome/browser/sync/token_migrator.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/net/gaia/gaia_constants.h"
@@ -78,9 +78,9 @@ ProfileSyncService::ProfileSyncService(ProfileSyncFactory* factory,
sync_service_url_(kDevServerUrl),
backend_initialized_(false),
is_auth_in_progress_(false),
- ALLOW_THIS_IN_INITIALIZER_LIST(wizard_(this)),
+ wizard_(ALLOW_THIS_IN_INITIALIZER_LIST(this)),
unrecoverable_error_detected_(false),
- ALLOW_THIS_IN_INITIALIZER_LIST(scoped_runnable_method_factory_(this)),
+ scoped_runnable_method_factory_(ALLOW_THIS_IN_INITIALIZER_LIST(this)),
token_migrator_(NULL),
clear_server_data_state_(CLEAR_NOT_STARTED) {
DCHECK(factory);
@@ -475,6 +475,8 @@ void ProfileSyncService::Shutdown(bool sync_disabled) {
data_type_manager_.reset();
}
+ js_event_handlers_.RemoveBackend();
+
// Move aside the backend so nobody else tries to use it while we are
// shutting it down.
scoped_ptr<SyncBackendHost> doomed_backend(backend_.release());
@@ -538,6 +540,8 @@ void ProfileSyncService::UpdateLastSyncedTime() {
void ProfileSyncService::NotifyObservers() {
FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged());
+ js_event_handlers_.RouteJsEvent(
+ "onSyncServiceStateChanged", browser_sync::JsArgList(), NULL);
}
// static
@@ -602,6 +606,8 @@ void ProfileSyncService::OnUnrecoverableError(
void ProfileSyncService::OnBackendInitialized() {
backend_initialized_ = true;
+ js_event_handlers_.SetBackend(backend_->GetJsBackend());
+
// The very first time the backend initializes is effectively the first time
// we can say we successfully "synced". last_synced_time_ will only be null
// in this case, because the pref wasn't restored on StartUp.
@@ -1282,6 +1288,10 @@ bool ProfileSyncService::HasObserver(Observer* observer) const {
return observers_.HasObserver(observer);
}
+browser_sync::JsFrontend* ProfileSyncService::GetJsFrontend() {
+ return &js_event_handlers_;
+}
+
void ProfileSyncService::SyncEvent(SyncEventCodes code) {
UMA_HISTOGRAM_ENUMERATION("Sync.EventCodes", code, MAX_SYNC_EVENT_CODE);
}
diff --git a/chrome/browser/sync/profile_sync_service.h b/chrome/browser/sync/profile_sync_service.h
index 90acae7..ec9ce6b 100644
--- a/chrome/browser/sync/profile_sync_service.h
+++ b/chrome/browser/sync/profile_sync_service.h
@@ -21,6 +21,7 @@
#include "chrome/browser/sync/glue/data_type_manager.h"
#include "chrome/browser/sync/glue/session_model_associator.h"
#include "chrome/browser/sync/glue/sync_backend_host.h"
+#include "chrome/browser/sync/js_event_handler_list.h"
#include "chrome/browser/sync/profile_sync_service_observer.h"
#include "chrome/browser/sync/signin_manager.h"
#include "chrome/browser/sync/sync_setup_wizard.h"
@@ -40,6 +41,10 @@ class ProfileSyncFactory;
class TabContents;
class TokenMigrator;
+namespace browser_sync {
+class JsFrontend;
+} // namespace browser_sync
+
// ProfileSyncService is the layer between browser subsystems like bookmarks,
// and the sync backend. Each subsystem is logically thought of as being
// a sync datatype.
@@ -306,6 +311,11 @@ class ProfileSyncService : public browser_sync::SyncFrontend,
// Returns true if |observer| has already been added as an observer.
bool HasObserver(Observer* observer) const;
+ // Returns a pointer to the service's JsFrontend (which is owned by
+ // the service). Never returns NULL. Overrideable for testing
+ // purposes.
+ virtual browser_sync::JsFrontend* GetJsFrontend();
+
// Record stats on various events.
static void SyncEvent(SyncEventCodes code);
@@ -581,6 +591,8 @@ class ProfileSyncService : public browser_sync::SyncFrontend,
ObserverList<Observer> observers_;
+ browser_sync::JsEventHandlerList js_event_handlers_;
+
NotificationRegistrar registrar_;
ScopedRunnableMethodFactory<ProfileSyncService>
diff --git a/chrome/browser/sync/profile_sync_service_mock.h b/chrome/browser/sync/profile_sync_service_mock.h
index 32bff66..4e460f2 100644
--- a/chrome/browser/sync/profile_sync_service_mock.h
+++ b/chrome/browser/sync/profile_sync_service_mock.h
@@ -43,6 +43,7 @@ class ProfileSyncServiceMock : public ProfileSyncService {
MOCK_METHOD0(InitializeBackend, void());
MOCK_METHOD1(AddObserver, void(Observer*));
MOCK_METHOD1(RemoveObserver, void(Observer*));
+ MOCK_METHOD0(GetJsFrontend, browser_sync::JsFrontend*());
MOCK_CONST_METHOD0(HasSyncSetupCompleted, bool());
MOCK_METHOD1(ChangePreferredDataTypes,
diff --git a/chrome/browser/sync/profile_sync_service_unittest.cc b/chrome/browser/sync/profile_sync_service_unittest.cc
index 15b93aa..ce2134e 100644
--- a/chrome/browser/sync/profile_sync_service_unittest.cc
+++ b/chrome/browser/sync/profile_sync_service_unittest.cc
@@ -13,6 +13,7 @@
#include "base/string_util.h"
#include "base/string16.h"
#include "base/utf_string_conversions.h"
+#include "base/values.h"
#include "chrome/browser/bookmarks/bookmark_model.h"
#include "chrome/browser/browser_thread.h"
#include "chrome/browser/net/gaia/token_service.h"
@@ -27,6 +28,8 @@
#include "chrome/browser/sync/glue/model_associator.h"
#include "chrome/browser/sync/glue/sync_backend_host.h"
#include "chrome/browser/sync/glue/sync_backend_host_mock.h"
+#include "chrome/browser/sync/js_arg_list.h"
+#include "chrome/browser/sync/js_test_util.h"
#include "chrome/browser/sync/profile_sync_factory.h"
#include "chrome/browser/sync/profile_sync_factory_mock.h"
#include "chrome/browser/sync/test_profile_sync_service.h"
@@ -43,12 +46,16 @@ using browser_sync::BookmarkChangeProcessor;
using browser_sync::BookmarkModelAssociator;
using browser_sync::ChangeProcessor;
using browser_sync::DataTypeController;
+using browser_sync::HasArgs;
+using browser_sync::JsArgList;
+using browser_sync::MockJsEventHandler;
using browser_sync::ModelAssociator;
using browser_sync::SyncBackendHost;
using browser_sync::SyncBackendHostMock;
using browser_sync::UnrecoverableErrorHandler;
using testing::_;
using testing::Return;
+using testing::StrictMock;
using testing::WithArg;
using testing::Invoke;
@@ -310,14 +317,16 @@ class ProfileSyncServiceTest : public testing::Test {
}
void StartSyncService() {
- StartSyncServiceAndSetInitialSyncEnded(true);
+ StartSyncServiceAndSetInitialSyncEnded(true, true);
}
- void StartSyncServiceAndSetInitialSyncEnded(bool set_initial_sync_ended) {
+ void StartSyncServiceAndSetInitialSyncEnded(
+ bool set_initial_sync_ended,
+ bool issue_auth_token) {
if (!service_.get()) {
// Set bootstrap to true and it will provide a logged in user for test
service_.reset(new TestProfileSyncService(&factory_,
profile_.get(),
- "test", false, NULL));
+ "test", true, NULL));
if (!set_initial_sync_ended)
service_->dont_set_initial_sync_ended_on_init();
@@ -337,10 +346,11 @@ class ProfileSyncServiceTest : public testing::Test {
profile_.get(),
service_.get()));
- profile_->GetTokenService()->IssueAuthTokenForTest(
- GaiaConstants::kSyncService, "token");
+ if (issue_auth_token) {
+ profile_->GetTokenService()->IssueAuthTokenForTest(
+ GaiaConstants::kSyncService, "token");
+ }
service_->Initialize();
- MessageLoop::current()->Run();
}
}
@@ -947,6 +957,170 @@ TEST_F(ProfileSyncServiceTest, MergeDuplicates) {
ExpectModelMatch();
}
+TEST_F(ProfileSyncServiceTest, JsFrontendHandlersBasic) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ StartSyncService();
+
+ StrictMock<MockJsEventHandler> event_handler;
+
+ browser_sync::SyncBackendHostForProfileSyncTest* test_backend =
+ service_->GetBackendForTest();
+
+ EXPECT_TRUE(service_->sync_initialized());
+ ASSERT_TRUE(test_backend != NULL);
+ ASSERT_TRUE(test_backend->GetJsBackend() != NULL);
+ EXPECT_EQ(NULL, test_backend->GetJsBackend()->GetParentJsEventRouter());
+
+ browser_sync::JsFrontend* js_backend = service_->GetJsFrontend();
+ js_backend->AddHandler(&event_handler);
+ ASSERT_TRUE(test_backend->GetJsBackend() != NULL);
+ EXPECT_TRUE(test_backend->GetJsBackend()->GetParentJsEventRouter() != NULL);
+
+ js_backend->RemoveHandler(&event_handler);
+ EXPECT_EQ(NULL, test_backend->GetJsBackend()->GetParentJsEventRouter());
+}
+
+TEST_F(ProfileSyncServiceTest,
+ JsFrontendHandlersDelayedBackendInitialization) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ StartSyncServiceAndSetInitialSyncEnded(true, false);
+
+ StrictMock<MockJsEventHandler> event_handler;
+ EXPECT_CALL(event_handler,
+ HandleJsEvent("onSyncServiceStateChanged",
+ HasArgs(JsArgList()))).Times(3);
+
+ EXPECT_EQ(NULL, service_->GetBackendForTest());
+ EXPECT_FALSE(service_->sync_initialized());
+
+ browser_sync::JsFrontend* js_backend = service_->GetJsFrontend();
+ js_backend->AddHandler(&event_handler);
+ // Since we're doing synchronous initialization, backend should be
+ // initialized by this call.
+ profile_->GetTokenService()->IssueAuthTokenForTest(
+ GaiaConstants::kSyncService, "token");
+
+ browser_sync::SyncBackendHostForProfileSyncTest* test_backend =
+ service_->GetBackendForTest();
+
+ EXPECT_TRUE(service_->sync_initialized());
+ ASSERT_TRUE(test_backend != NULL);
+ ASSERT_TRUE(test_backend->GetJsBackend() != NULL);
+ EXPECT_TRUE(test_backend->GetJsBackend()->GetParentJsEventRouter() != NULL);
+
+ js_backend->RemoveHandler(&event_handler);
+ EXPECT_EQ(NULL, test_backend->GetJsBackend()->GetParentJsEventRouter());
+}
+
+TEST_F(ProfileSyncServiceTest, JsFrontendProcessMessageBasic) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ StartSyncService();
+
+ StrictMock<MockJsEventHandler> event_handler;
+
+ ListValue arg_list1;
+ arg_list1.Append(Value::CreateBooleanValue(true));
+ arg_list1.Append(Value::CreateIntegerValue(5));
+ JsArgList args1(arg_list1);
+ EXPECT_CALL(event_handler, HandleJsEvent("testMessage1", HasArgs(args1)));
+
+ ListValue arg_list2;
+ arg_list2.Append(Value::CreateStringValue("test"));
+ arg_list2.Append(arg_list1.DeepCopy());
+ JsArgList args2(arg_list2);
+ EXPECT_CALL(event_handler,
+ HandleJsEvent("delayTestMessage2", HasArgs(args2)));
+
+ ListValue arg_list3;
+ arg_list3.Append(arg_list1.DeepCopy());
+ arg_list3.Append(arg_list2.DeepCopy());
+ JsArgList args3(arg_list3);
+
+ browser_sync::JsFrontend* js_backend = service_->GetJsFrontend();
+
+ // Never replied to.
+ js_backend->ProcessMessage("notRepliedTo", args3, &event_handler);
+
+ // Replied to later.
+ js_backend->ProcessMessage("delayTestMessage2", args2, &event_handler);
+
+ js_backend->AddHandler(&event_handler);
+
+ // Replied to immediately.
+ js_backend->ProcessMessage("testMessage1", args1, &event_handler);
+
+ // Fires off reply for delayTestMessage2.
+ message_loop_.RunAllPending();
+
+ // Never replied to.
+ js_backend->ProcessMessage("delayNotRepliedTo", args3, &event_handler);
+
+ js_backend->RemoveHandler(&event_handler);
+
+ message_loop_.RunAllPending();
+
+ // Never replied to.
+ js_backend->ProcessMessage("notRepliedTo", args3, &event_handler);
+}
+
+TEST_F(ProfileSyncServiceTest,
+ JsFrontendProcessMessageBasicDelayedBackendInitialization) {
+ LoadBookmarkModel(DELETE_EXISTING_STORAGE, DONT_SAVE_TO_STORAGE);
+ StartSyncServiceAndSetInitialSyncEnded(true, false);
+
+ StrictMock<MockJsEventHandler> event_handler;
+
+ ListValue arg_list1;
+ arg_list1.Append(Value::CreateBooleanValue(true));
+ arg_list1.Append(Value::CreateIntegerValue(5));
+ JsArgList args1(arg_list1);
+ EXPECT_CALL(event_handler, HandleJsEvent("testMessage1", HasArgs(args1)));
+
+ ListValue arg_list2;
+ arg_list2.Append(Value::CreateStringValue("test"));
+ arg_list2.Append(arg_list1.DeepCopy());
+ JsArgList args2(arg_list2);
+ EXPECT_CALL(event_handler, HandleJsEvent("testMessage2", HasArgs(args2)));
+
+ ListValue arg_list3;
+ arg_list3.Append(arg_list1.DeepCopy());
+ arg_list3.Append(arg_list2.DeepCopy());
+ JsArgList args3(arg_list3);
+ EXPECT_CALL(event_handler,
+ HandleJsEvent("delayTestMessage3", HasArgs(args3)));
+
+ const JsArgList kNoArgs;
+
+ EXPECT_CALL(event_handler, HandleJsEvent("onSyncServiceStateChanged",
+ HasArgs(kNoArgs))).Times(3);
+
+ browser_sync::JsFrontend* js_backend = service_->GetJsFrontend();
+
+ // We expect a reply for this message, even though its sent before
+ // |event_handler| is added as a handler.
+ js_backend->ProcessMessage("testMessage1", args1, &event_handler);
+
+ js_backend->AddHandler(&event_handler);
+
+ js_backend->ProcessMessage("testMessage2", args2, &event_handler);
+ js_backend->ProcessMessage("delayTestMessage3", args3, &event_handler);
+
+ // Fires testMessage1 and testMessage2.
+ profile_->GetTokenService()->IssueAuthTokenForTest(
+ GaiaConstants::kSyncService, "token");
+
+ // Fires delayTestMessage3.
+ message_loop_.RunAllPending();
+
+ js_backend->ProcessMessage("delayNotRepliedTo", kNoArgs, &event_handler);
+
+ js_backend->RemoveHandler(&event_handler);
+
+ message_loop_.RunAllPending();
+
+ js_backend->ProcessMessage("notRepliedTo", kNoArgs, &event_handler);
+}
+
struct TestData {
const wchar_t* title;
const char* url;
@@ -1401,7 +1575,7 @@ TEST_F(ProfileSyncServiceTestWithData, RecoverAfterDeletingSyncDataDirectory) {
// Restart the sync service. Don't fake out setting initial sync ended; lets
// make sure the system does in fact nudge and wait for this to happen.
- StartSyncServiceAndSetInitialSyncEnded(false);
+ StartSyncServiceAndSetInitialSyncEnded(false, true);
// Make sure we're back in sync. In real life, the user would need
// to reauthenticate before this happens, but in the test, authentication
diff --git a/chrome/browser/sync/test_profile_sync_service.cc b/chrome/browser/sync/test_profile_sync_service.cc
index 0b9ec18..e536282 100644
--- a/chrome/browser/sync/test_profile_sync_service.cc
+++ b/chrome/browser/sync/test_profile_sync_service.cc
@@ -111,6 +111,40 @@ void SyncBackendHostForProfileSyncTest::InitCore(
}
}
+JsBackend* SyncBackendHostForProfileSyncTest::GetJsBackend() {
+ // Return a non-NULL result only when the overridden function does.
+ if (SyncBackendHost::GetJsBackend()) {
+ return this;
+ } else {
+ NOTREACHED();
+ return NULL;
+ }
+}
+
+void SyncBackendHostForProfileSyncTest::SetParentJsEventRouter(
+ JsEventRouter* router) {
+ core_->SetParentJsEventRouter(router);
+}
+
+void SyncBackendHostForProfileSyncTest::RemoveParentJsEventRouter() {
+ core_->RemoveParentJsEventRouter();
+}
+
+const JsEventRouter*
+ SyncBackendHostForProfileSyncTest::GetParentJsEventRouter() const {
+ return core_->GetParentJsEventRouter();
+}
+
+void SyncBackendHostForProfileSyncTest::ProcessMessage(
+ const std::string& name, const JsArgList& args,
+ const JsEventHandler* sender) {
+ if (name.find("delay") != name.npos) {
+ core_->RouteJsEvent(name, args, sender);
+ } else {
+ core_->RouteJsEventOnFrontendLoop(name, args, sender);
+ }
+}
+
void SyncBackendHostForProfileSyncTest::
SetDefaultExpectationsForWorkerCreation(ProfileMock* profile) {
EXPECT_CALL(*profile, GetPasswordStore(testing::_)).
diff --git a/chrome/browser/sync/test_profile_sync_service.h b/chrome/browser/sync/test_profile_sync_service.h
index 452b1ed..377b05c 100644
--- a/chrome/browser/sync/test_profile_sync_service.h
+++ b/chrome/browser/sync/test_profile_sync_service.h
@@ -9,6 +9,7 @@
#include <string>
#include "chrome/browser/sync/glue/data_type_manager_impl.h"
+#include "chrome/browser/sync/js_backend.h"
#include "chrome/browser/sync/profile_sync_service.h"
#include "chrome/test/profile_mock.h"
#include "chrome/test/sync/engine/test_id_factory.h"
@@ -28,7 +29,8 @@ namespace browser_sync {
// running in these tests, and allows tests to provide a task on construction
// to set up initial nodes to mock out an actual server initial sync
// download.
-class SyncBackendHostForProfileSyncTest : public SyncBackendHost {
+class SyncBackendHostForProfileSyncTest
+ : public SyncBackendHost, public JsBackend {
public:
// |synchronous_init| causes initialization to block until the syncapi has
// completed setting itself up and called us back.
@@ -56,6 +58,18 @@ class SyncBackendHostForProfileSyncTest : public SyncBackendHost {
virtual void InitCore(const Core::DoInitializeOptions& options);
+ virtual JsBackend* GetJsBackend();
+
+ // JsBackend implementation.
+ virtual void SetParentJsEventRouter(JsEventRouter* router);
+ virtual void RemoveParentJsEventRouter();
+ virtual const JsEventRouter* GetParentJsEventRouter() const;
+ // Fires an event identical to the message unless the message has
+ // "delay" as a prefix, in which case a task to fire the identical
+ // event is posted instead.
+ virtual void ProcessMessage(const std::string& name, const JsArgList& args,
+ const JsEventHandler* sender);
+
static void SetDefaultExpectationsForWorkerCreation(ProfileMock* profile);
static void SetHistoryServiceExpectations(ProfileMock* profile);
diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp
index 7b7eeed..0f1db96 100644
--- a/chrome/chrome.gyp
+++ b/chrome/chrome.gyp
@@ -956,6 +956,14 @@
'browser/sync/engine/update_applicator.h',
'browser/sync/engine/verify_updates_command.cc',
'browser/sync/engine/verify_updates_command.h',
+ 'browser/sync/js_arg_list.cc',
+ 'browser/sync/js_arg_list.h',
+ 'browser/sync/js_backend.h',
+ 'browser/sync/js_event_handler.h',
+ 'browser/sync/js_event_handler_list.cc',
+ 'browser/sync/js_event_handler_list.h',
+ 'browser/sync/js_event_router.h',
+ 'browser/sync/js_frontend.h',
'browser/sync/protocol/service_constants.h',
'browser/sync/sessions/ordered_commit_set.cc',
'browser/sync/sessions/ordered_commit_set.h',
diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi
index e0059c1..7c33c93 100644
--- a/chrome/chrome_browser.gypi
+++ b/chrome/chrome_browser.gypi
@@ -979,8 +979,6 @@
'browser/dom_ui/slideshow_ui.h',
'browser/dom_ui/sync_internals_html_source.cc',
'browser/dom_ui/sync_internals_html_source.h',
- 'browser/dom_ui/sync_internals_message_handler.cc',
- 'browser/dom_ui/sync_internals_message_handler.h',
'browser/dom_ui/sync_internals_ui.cc',
'browser/dom_ui/sync_internals_ui.h',
'browser/dom_ui/textfields_ui.cc',
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index 7c719ef..cdd303c 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -216,6 +216,22 @@
],
},
{
+ 'target_name': 'test_support_sync',
+ 'type': '<(library)',
+ 'dependencies': [
+ '../testing/gmock.gyp:gmock',
+ '../testing/gtest.gyp:gtest',
+ 'sync',
+ ],
+ 'include_dirs': [
+ '..',
+ ],
+ 'sources': [
+ 'browser/sync/js_test_util.cc',
+ 'browser/sync/js_test_util.h',
+ ],
+ },
+ {
'target_name': 'test_support_unit',
'type': '<(library)',
'dependencies': [
@@ -988,6 +1004,7 @@
'renderer',
'service',
'test_support_common',
+ 'test_support_sync',
'test_support_unit',
'utility',
'../app/app.gyp:app_base',
@@ -1157,6 +1174,7 @@
'browser/dom_ui/html_dialog_tab_contents_delegate_unittest.cc',
'browser/dom_ui/options/language_options_handler_unittest.cc',
'browser/dom_ui/shown_sections_handler_unittest.cc',
+ 'browser/dom_ui/sync_internals_ui_unittest.cc',
'browser/download/base_file_unittest.cc',
'browser/download/download_manager_unittest.cc',
'browser/download/download_request_infobar_delegate_unittest.cc',
@@ -2724,6 +2742,8 @@
'browser/sync/engine/syncproto_unittest.cc',
'browser/sync/engine/syncapi_mock.h',
'browser/sync/engine/verify_updates_command_unittest.cc',
+ 'browser/sync/js_arg_list_unittest.cc',
+ 'browser/sync/js_event_handler_list_unittest.cc',
'browser/sync/notifier/cache_invalidation_packet_handler_unittest.cc',
'browser/sync/notifier/chrome_invalidation_client_unittest.cc',
'browser/sync/notifier/chrome_system_resources_unittest.cc',
@@ -2781,6 +2801,7 @@
'profile_import',
'syncapi',
'sync_notifier',
+ 'test_support_sync',
'test_support_unit',
],
'conditions': [
diff --git a/chrome/test/profile_mock.h b/chrome/test/profile_mock.h
index 73770c8..d44a83e 100644
--- a/chrome/test/profile_mock.h
+++ b/chrome/test/profile_mock.h
@@ -22,6 +22,7 @@ class ProfileMock : public TestingProfile {
MOCK_METHOD0(GetPersonalDataManager, PersonalDataManager*());
MOCK_METHOD1(GetPasswordStore, PasswordStore* (ServiceAccessType access));
MOCK_METHOD0(GetTokenService, TokenService*());
+ MOCK_METHOD0(GetProfileSyncService, ProfileSyncService*());
};
#endif // CHROME_TEST_PROFILE_MOCK_H__