diff options
author | aa@chromium.org <aa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-04-14 23:40:41 +0000 |
---|---|---|
committer | aa@chromium.org <aa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-04-14 23:40:41 +0000 |
commit | 4c29efd402105cd29908d21550829bc089a8fe36 (patch) | |
tree | e2e2ac0f5a5d47db3417c1e85205c57ede5e7cb8 | |
parent | 5ea8fc38c5cc261077b61469d8ccb158f3e347b8 (diff) | |
download | chromium_src-4c29efd402105cd29908d21550829bc089a8fe36.zip chromium_src-4c29efd402105cd29908d21550829bc089a8fe36.tar.gz chromium_src-4c29efd402105cd29908d21550829bc089a8fe36.tar.bz2 |
Add JsonSchema-based validation for the tab APIs.
Arv: can you take json_schema.js and json_schema_test.js.
Matt: you take the rest.
Review URL: http://codereview.chromium.org/66006
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@13720 0039d316-1c4b-4281-b951-d872f2087c98
20 files changed, 1350 insertions, 227 deletions
diff --git a/chrome/browser/extensions/extension_messages_unittest.cc b/chrome/browser/extensions/extension_messages_unittest.cc new file mode 100644 index 0000000..565f664 --- /dev/null +++ b/chrome/browser/extensions/extension_messages_unittest.cc @@ -0,0 +1,95 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/common/render_messages.h" +#include "chrome/renderer/extensions/renderer_extension_bindings.h" +#include "chrome/test/render_view_test.h" +#include "testing/gtest/include/gtest/gtest.h" + +// Tests that the bindings for opening a channel to an extension and sending +// and receiving messages through that channel all works. +TEST_F(RenderViewTest, ExtensionMessagesOpenChannel) { + render_thread_.sink().ClearMessages(); + LoadHTML("<body></body>"); + ExecuteJavaScript( + "var e = new chromium.Extension('foobar');" + "var port = e.connect();" + "port.onmessage.addListener(doOnMessage);" + "port.postMessage({message: 'content ready'});" + "function doOnMessage(msg, port) {" + " alert('content got: ' + msg.val);" + "}"); + + // Verify that we opened a channel and sent a message through it. + const IPC::Message* open_channel_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_OpenChannelToExtension::ID); + EXPECT_TRUE(open_channel_msg); + + const IPC::Message* post_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_ExtensionPostMessage::ID); + ASSERT_TRUE(post_msg); + ViewHostMsg_ExtensionPostMessage::Param post_params; + ViewHostMsg_ExtensionPostMessage::Read(post_msg, &post_params); + EXPECT_EQ("{\"message\":\"content ready\"}", post_params.b); + + // Now simulate getting a message back from the other side. + render_thread_.sink().ClearMessages(); + const int kPortId = 0; + RendererExtensionBindings::HandleMessage("{\"val\": 42}", kPortId); + + // Verify that we got it. + const IPC::Message* alert_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_RunJavaScriptMessage::ID); + ASSERT_TRUE(alert_msg); + void* iter = IPC::SyncMessage::GetDataIterator(alert_msg); + ViewHostMsg_RunJavaScriptMessage::SendParam alert_param; + IPC::ReadParam(alert_msg, &iter, &alert_param); + EXPECT_EQ(L"content got: 42", alert_param.a); +} + +// Tests that the bindings for handling a new channel connection and sending +// and receiving messages through that channel all works. +TEST_F(RenderViewTest, ExtensionMessagesOnConnect) { + LoadHTML("<body></body>"); + ExecuteJavaScript( + "chromium.onconnect.addListener(function (port) {" + " port.onmessage.addListener(doOnMessage);" + " port.postMessage({message: 'onconnect'});" + "});" + "function doOnMessage(msg, port) {" + " alert('got: ' + msg.val);" + "}"); + + render_thread_.sink().ClearMessages(); + + // Simulate a new connection being opened. + const int kPortId = 0; + RendererExtensionBindings::HandleConnect(kPortId); + + // Verify that we handled the new connection by posting a message. + const IPC::Message* post_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_ExtensionPostMessage::ID); + ASSERT_TRUE(post_msg); + ViewHostMsg_ExtensionPostMessage::Param post_params; + ViewHostMsg_ExtensionPostMessage::Read(post_msg, &post_params); + EXPECT_EQ("{\"message\":\"onconnect\"}", post_params.b); + + // Now simulate getting a message back from the channel opener. + render_thread_.sink().ClearMessages(); + RendererExtensionBindings::HandleMessage("{\"val\": 42}", kPortId); + + // Verify that we got it. + const IPC::Message* alert_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_RunJavaScriptMessage::ID); + ASSERT_TRUE(alert_msg); + void* iter = IPC::SyncMessage::GetDataIterator(alert_msg); + ViewHostMsg_RunJavaScriptMessage::SendParam alert_param; + IPC::ReadParam(alert_msg, &iter, &alert_param); + EXPECT_EQ(L"got: 42", alert_param.a); +} diff --git a/chrome/browser/extensions/extension_view_unittest.cc b/chrome/browser/extensions/extension_view_unittest.cc index a1dd565..360df57 100755 --- a/chrome/browser/extensions/extension_view_unittest.cc +++ b/chrome/browser/extensions/extension_view_unittest.cc @@ -11,8 +11,8 @@ #include "chrome/browser/extensions/extensions_service.h" #include "chrome/browser/extensions/test_extension_loader.h" #include "chrome/browser/tab_contents/site_instance.h" -#include "chrome/common/chrome_switches.h" #include "chrome/common/chrome_paths.h" +#include "chrome/common/chrome_switches.h" #include "chrome/test/in_process_browser_test.h" #include "chrome/test/ui_test_utils.h" diff --git a/chrome/chrome.gyp b/chrome/chrome.gyp index 8e1be3e..2388f19 100644 --- a/chrome/chrome.gyp +++ b/chrome/chrome.gyp @@ -2224,7 +2224,9 @@ 'common/unzip_unittest.cc', 'common/win_util_unittest.cc', 'common/worker_thread_ticker_unittest.cc', + 'renderer/extensions/extension_api_client_unittest.cc', 'renderer/extensions/greasemonkey_api_unittest.cc', + 'renderer/extensions/json_schema_unittest.cc', 'renderer/net/render_dns_master_unittest.cc', 'renderer/net/render_dns_queue_unittest.cc', 'renderer/render_process_unittest.cc', @@ -2237,6 +2239,8 @@ 'test/browser_with_test_window_test.h', 'test/in_process_browser_test.cc', 'test/in_process_browser_test.h', + 'test/render_view_test.cc', + 'test/render_view_test.h', 'test/test_notification_tracker.cc', 'test/test_notification_tracker.h', 'test/v8_unit_test.cc', @@ -2314,6 +2318,7 @@ 'browser/bookmarks/bookmark_table_model_unittest.cc', 'browser/browser_commands_unittest.cc', 'browser/extensions/extension_content_script_inject_unittest.cc', + 'browser/extensions/extension_messages_unittest.cc', 'browser/extensions/test_extension_loader.cc', 'browser/extensions/user_script_master_unittest.cc', 'browser/importer/firefox_importer_unittest.cc', diff --git a/chrome/renderer/extensions/extension_api_client_unittest.cc b/chrome/renderer/extensions/extension_api_client_unittest.cc new file mode 100755 index 0000000..1c3530f --- /dev/null +++ b/chrome/renderer/extensions/extension_api_client_unittest.cc @@ -0,0 +1,186 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/common/render_messages.h" +#include "chrome/renderer/extensions/extension_process_bindings.h" +#include "chrome/renderer/extensions/renderer_extension_bindings.h" +#include "chrome/test/render_view_test.h" +#include "testing/gtest/include/gtest/gtest.h" + +class ExtensionAPIClientTest : public RenderViewTest { + protected: + virtual void SetUp() { + RenderViewTest::SetUp(); + + render_thread_.sink().ClearMessages(); + LoadHTML("<body></body>"); + } + + std::string GetConsoleMessage() { + const IPC::Message* message = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_AddMessageToConsole::ID); + ViewHostMsg_AddMessageToConsole::Param params; + if (message) { + ViewHostMsg_AddMessageToConsole::Read(message, ¶ms); + render_thread_.sink().ClearMessages(); + return WideToASCII(params.a); + } else { + return ""; + } + } + + void ExpectJsFail(const std::string& js, const std::string& message) { + ExecuteJavaScript(js.c_str()); + EXPECT_EQ(message, GetConsoleMessage()); + } +}; + +// Tests that callback dispatching works correctly and that JSON is properly +// deserialized before handing off to the extension code. We use the createTab +// API here, but we could use any of them since they all dispatch callbacks the +// same way. +TEST_F(ExtensionAPIClientTest, CallbackDispatching) { + ExecuteJavaScript( + "function assert(truth, message) {" + " if (!truth) {" + " throw new Error(message);" + " }" + "}" + "function callback(result) {" + " assert(typeof result == 'object', 'result not object');" + " assert(goog.json.serialize(result) == '{\"foo\":\"bar\"}', " + " 'incorrect result');" + " console.log('pass')" + "}" + "chromium.tabs.createTab({}, callback);" + ); + + // Ok, we should have gotten a message to create a tab, grab the callback ID. + const IPC::Message* request_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_ExtensionRequest::ID); + ASSERT_TRUE(request_msg); + ViewHostMsg_ExtensionRequest::Param params; + ViewHostMsg_ExtensionRequest::Read(request_msg, ¶ms); + int callback_id = params.c; + ASSERT_TRUE(callback_id >= 0); + + // Now send the callback a response + ExtensionProcessBindings::ExecuteCallbackInFrame( + GetMainFrame(), callback_id, "{\"foo\":\"bar\"}"); + + // And verify that it worked + ASSERT_EQ("pass", GetConsoleMessage()); +} + +// The remainder of these tests exercise the client side of the various +// extension functions. We test both error and success conditions, but do not +// test errors exhaustively as json schema code is well tested by itself. + +TEST_F(ExtensionAPIClientTest, GetTabsForWindow) { + ExpectJsFail("chromium.tabs.getTabsForWindow(42, function(){});", + "Uncaught Error: Too many arguments."); + + ExecuteJavaScript("chromium.tabs.getTabsForWindow(function(){})"); + const IPC::Message* request_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_ExtensionRequest::ID); + ASSERT_TRUE(request_msg); + ViewHostMsg_ExtensionRequest::Param params; + ViewHostMsg_ExtensionRequest::Read(request_msg, ¶ms); + ASSERT_EQ("GetTabsForWindow", params.a); + ASSERT_EQ("null", params.b); +} + +TEST_F(ExtensionAPIClientTest, GetTab) { + ExpectJsFail("chromium.tabs.getTab(null, function(){});", + "Uncaught Error: Argument 0 is required."); + + ExecuteJavaScript("chromium.tabs.getTab(42)"); + const IPC::Message* request_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_ExtensionRequest::ID); + ASSERT_TRUE(request_msg); + ViewHostMsg_ExtensionRequest::Param params; + ViewHostMsg_ExtensionRequest::Read(request_msg, ¶ms); + ASSERT_EQ("GetTab", params.a); + ASSERT_EQ("42", params.b); +} + +TEST_F(ExtensionAPIClientTest, CreateTab) { + ExpectJsFail("chromium.tabs.createTab({windowId: 'foo'}, function(){});", + "Uncaught Error: Invalid value for argument 0. Property " + "'windowId': Expected 'integer' but got 'string'."); + ExpectJsFail("chromium.tabs.createTab({url: 42}, function(){});", + "Uncaught Error: Invalid value for argument 0. Property " + "'url': Expected 'string' but got 'integer'."); + ExpectJsFail("chromium.tabs.createTab({selected: null}, function(){});", + "Uncaught Error: Invalid value for argument 0. Property " + "'selected': Expected 'boolean' but got 'null'."); + + ExecuteJavaScript("chromium.tabs.createTab({" + " url:'http://www.google.com/'," + " selected:true," + " windowId:4" + "})"); + const IPC::Message* request_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_ExtensionRequest::ID); + ASSERT_TRUE(request_msg); + ViewHostMsg_ExtensionRequest::Param params; + ViewHostMsg_ExtensionRequest::Read(request_msg, ¶ms); + ASSERT_EQ("CreateTab", params.a); + ASSERT_EQ("{\"url\":\"http://www.google.com/\"," + "\"selected\":true," + "\"windowId\":4}", params.b); +} + +TEST_F(ExtensionAPIClientTest, UpdateTab) { + ExpectJsFail("chromium.tabs.updateTab({id: null});", + "Uncaught Error: Invalid value for argument 0. Property " + "'id': Expected 'integer' but got 'null'."); + ExpectJsFail("chromium.tabs.updateTab({id: 42, windowId: 'foo'});", + "Uncaught Error: Invalid value for argument 0. Property " + "'windowId': Expected 'integer' but got 'string'."); + ExpectJsFail("chromium.tabs.updateTab({id: 42, url: 42});", + "Uncaught Error: Invalid value for argument 0. Property " + "'url': Expected 'string' but got 'integer'."); + ExpectJsFail("chromium.tabs.updateTab({id: 42, selected: null});", + "Uncaught Error: Invalid value for argument 0. Property " + "'selected': Expected 'boolean' but got 'null'."); + + ExecuteJavaScript("chromium.tabs.updateTab({" + " id:42," + " url:'http://www.google.com/'," + " selected:true," + " windowId:4" + "})"); + const IPC::Message* request_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_ExtensionRequest::ID); + ASSERT_TRUE(request_msg); + ViewHostMsg_ExtensionRequest::Param params; + ViewHostMsg_ExtensionRequest::Read(request_msg, ¶ms); + ASSERT_EQ("UpdateTab", params.a); + ASSERT_EQ("{\"id\":42," + "\"url\":\"http://www.google.com/\"," + "\"selected\":true," + "\"windowId\":4}", params.b); +} + +TEST_F(ExtensionAPIClientTest, RemoveTab) { + ExpectJsFail("chromium.tabs.removeTab('foobar', function(){});", + "Uncaught Error: Too many arguments."); + + ExecuteJavaScript("chromium.tabs.removeTab(21)"); + const IPC::Message* request_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_ExtensionRequest::ID); + ASSERT_TRUE(request_msg); + ViewHostMsg_ExtensionRequest::Param params; + ViewHostMsg_ExtensionRequest::Read(request_msg, ¶ms); + ASSERT_EQ("RemoveTab", params.a); + ASSERT_EQ("21", params.b); +} diff --git a/chrome/renderer/extensions/extension_process_bindings.cc b/chrome/renderer/extensions/extension_process_bindings.cc index 2d8f3db..0e47b23 100644 --- a/chrome/renderer/extensions/extension_process_bindings.cc +++ b/chrome/renderer/extensions/extension_process_bindings.cc @@ -18,14 +18,17 @@ using WebKit::WebString; namespace { const char kExtensionName[] = "chrome/ExtensionProcessBindings"; -const char* kExtensionDeps[] = { JsonJsV8Extension::kName }; +const char* kExtensionDeps[] = { + BaseJsV8Extension::kName, + JsonJsV8Extension::kName, + JsonSchemaJsV8Extension::kName +}; class ExtensionImpl : public v8::Extension { public: ExtensionImpl() : v8::Extension( kExtensionName, GetStringResource<IDR_EXTENSION_PROCESS_BINDINGS_JS>(), - arraysize(kExtensionDeps), kExtensionDeps) { - } + arraysize(kExtensionDeps), kExtensionDeps) {} static void SetFunctionNames(const std::vector<std::string>& names) { function_names_ = new std::set<std::string>(); diff --git a/chrome/renderer/extensions/extension_process_bindings.h b/chrome/renderer/extensions/extension_process_bindings.h index 8246b4b..51b13a7 100644 --- a/chrome/renderer/extensions/extension_process_bindings.h +++ b/chrome/renderer/extensions/extension_process_bindings.h @@ -1,25 +1,25 @@ -// Copyright (c) 2009 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// Exposes extension APIs into the extension process.
-
-#ifndef CHROME_RENDERER_EXTENSIONS_EXTENSION_PROCESS_BINDINGS_H_
-#define CHROME_RENDERER_EXTENSIONS_EXTENSION_PROCESS_BINDINGS_H_
-
-#include <string>
-#include <vector>
-
-#include "v8/include/v8.h"
-
-class WebFrame;
-
-class ExtensionProcessBindings {
- public:
- static void SetFunctionNames(const std::vector<std::string>& names);
- static v8::Extension* Get();
- static void ExecuteCallbackInFrame(WebFrame* frame, int callback_id,
- const std::string& response);
-};
-
-#endif // CHROME_RENDERER_EXTENSIONS_EXTENSION_PROCESS_BINDINGS_H_
+// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Exposes extension APIs into the extension process. + +#ifndef CHROME_RENDERER_EXTENSIONS_EXTENSION_PROCESS_BINDINGS_H_ +#define CHROME_RENDERER_EXTENSIONS_EXTENSION_PROCESS_BINDINGS_H_ + +#include <string> +#include <vector> + +#include "v8/include/v8.h" + +class WebFrame; + +class ExtensionProcessBindings { + public: + static void SetFunctionNames(const std::vector<std::string>& names); + static v8::Extension* Get(); + static void ExecuteCallbackInFrame(WebFrame* frame, int callback_id, + const std::string& response); +}; + +#endif // CHROME_RENDERER_EXTENSIONS_EXTENSION_PROCESS_BINDINGS_H_ diff --git a/chrome/renderer/extensions/json_schema_unittest.cc b/chrome/renderer/extensions/json_schema_unittest.cc new file mode 100755 index 0000000..200c435 --- /dev/null +++ b/chrome/renderer/extensions/json_schema_unittest.cc @@ -0,0 +1,79 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/file_util.h" +#include "base/path_service.h" +#include "base/string_util.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/common/resource_bundle.h" +#include "chrome/test/v8_unit_test.h" +#include "testing/gtest/include/gtest/gtest.h" + +#include "grit/renderer_resources.h" + +static const char kJsonSchema[] = "json_schema.js"; +static const char kJsonSchemaTest[] = "json_schema_test.js"; + +class JsonSchemaTest : public V8UnitTest { + public: + JsonSchemaTest() {} + + virtual void SetUp() { + V8UnitTest::SetUp(); + + // Add the json schema code to the context. + StringPiece js = ResourceBundle::GetSharedInstance().GetRawDataResource( + IDR_JSON_SCHEMA_JS); + ExecuteScriptInContext(js, kJsonSchema); + + // Add the test functions to the context. + std::wstring test_js_file_path; + ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &test_js_file_path)); + file_util::AppendToPath(&test_js_file_path, L"extensions"); + file_util::AppendToPath(&test_js_file_path, UTF8ToWide(kJsonSchemaTest)); + std::string test_js; + ASSERT_TRUE(file_util::ReadFileToString(test_js_file_path, &test_js)); + ExecuteScriptInContext(test_js, kJsonSchemaTest); + } +}; + +TEST_F(JsonSchemaTest, TestFormatError) { + TestFunction("testFormatError"); +} + +TEST_F(JsonSchemaTest, TestComplex) { + TestFunction("testComplex"); +} + +TEST_F(JsonSchemaTest, TestEnum) { + TestFunction("testEnum"); +} + +TEST_F(JsonSchemaTest, TestExtends) { + TestFunction("testExtends"); +} + +TEST_F(JsonSchemaTest, TestObject) { + TestFunction("testObject"); +} + +TEST_F(JsonSchemaTest, TestArrayTuple) { + TestFunction("testArrayTuple"); +} + +TEST_F(JsonSchemaTest, TestArrayNonTuple) { + TestFunction("testArrayNonTuple"); +} + +TEST_F(JsonSchemaTest, TestString) { + TestFunction("testString"); +} + +TEST_F(JsonSchemaTest, TestNumber) { + TestFunction("testNumber"); +} + +TEST_F(JsonSchemaTest, TestType) { + TestFunction("testType"); +} diff --git a/chrome/renderer/js_only_v8_extensions.cc b/chrome/renderer/js_only_v8_extensions.cc index 5ca3ae7..d304ef1 100644 --- a/chrome/renderer/js_only_v8_extensions.cc +++ b/chrome/renderer/js_only_v8_extensions.cc @@ -5,6 +5,7 @@ #include "chrome/renderer/js_only_v8_extensions.h" #include "chrome/renderer/extensions/bindings_utils.h" +#include "grit/renderer_resources.h" #include "grit/webkit_resources.h" // BaseJsV8Extension @@ -23,3 +24,10 @@ v8::Extension* JsonJsV8Extension::Get() { return new v8::Extension(kName, GetStringResource<IDR_DEVTOOLS_JSON_JS>(), arraysize(deps), deps); } + +// JsonSchemaJsV8Extension +const char* JsonSchemaJsV8Extension::kName = "chrome/jsonschema"; +v8::Extension* JsonSchemaJsV8Extension::Get() { + return new v8::Extension(kName, GetStringResource<IDR_JSON_SCHEMA_JS>(), + 0, NULL); +} diff --git a/chrome/renderer/js_only_v8_extensions.h b/chrome/renderer/js_only_v8_extensions.h index d7fc7a8..771103e 100644 --- a/chrome/renderer/js_only_v8_extensions.h +++ b/chrome/renderer/js_only_v8_extensions.h @@ -22,4 +22,10 @@ class JsonJsV8Extension { static v8::Extension* Get(); }; +class JsonSchemaJsV8Extension { + public: + static const char* kName; + static v8::Extension* Get(); +}; + #endif // CHROME_RENDERER_JS_ONLY_V8_EXTENSIONS_H_ diff --git a/chrome/renderer/render_thread.cc b/chrome/renderer/render_thread.cc index cdce9c6..e1bf797 100644 --- a/chrome/renderer/render_thread.cc +++ b/chrome/renderer/render_thread.cc @@ -299,6 +299,7 @@ void RenderThread::EnsureWebKitInitialized() { if (command_line.HasSwitch(switches::kEnableExtensions)) { WebKit::registerExtension(BaseJsV8Extension::Get()); WebKit::registerExtension(JsonJsV8Extension::Get()); + WebKit::registerExtension(JsonSchemaJsV8Extension::Get()); WebKit::registerExtension(EventBindings::Get()); WebKit::registerExtension(RendererExtensionBindings::Get(this)); } diff --git a/chrome/renderer/render_view.cc b/chrome/renderer/render_view.cc index 3dd7887..624f9d9 100644 --- a/chrome/renderer/render_view.cc +++ b/chrome/renderer/render_view.cc @@ -3050,8 +3050,6 @@ void RenderView::SendExtensionRequest(const std::string& name, const std::string& args, int callback_id, WebFrame* callback_frame) { - DCHECK(RenderThread::current()->message_loop() == MessageLoop::current()); - if (callback_id != -1) { DCHECK(callback_frame) << "Callback specified without frame"; pending_extension_callbacks_.AddWithID(callback_frame, callback_id); diff --git a/chrome/renderer/render_view_unittest.cc b/chrome/renderer/render_view_unittest.cc index f28b8d0..3be31bd 100644 --- a/chrome/renderer/render_view_unittest.cc +++ b/chrome/renderer/render_view_unittest.cc @@ -2,107 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "base/scoped_ptr.h" #include "chrome/common/render_messages.h" -#include "chrome/renderer/extensions/event_bindings.h" -#include "chrome/renderer/extensions/renderer_extension_bindings.h" -#include "chrome/renderer/js_only_v8_extensions.h" -#include "chrome/renderer/mock_render_process.h" -#include "chrome/renderer/mock_render_thread.h" -#include "chrome/renderer/render_view.h" -#include "chrome/renderer/renderer_webkitclient_impl.h" +#include "chrome/test/render_view_test.h" #include "testing/gtest/include/gtest/gtest.h" -#include "third_party/WebKit/WebKit/chromium/public/WebKit.h" -#include "third_party/WebKit/WebKit/chromium/public/WebScriptSource.h" -#include "webkit/glue/webframe.h" -#include "webkit/glue/weburlrequest.h" -#include "webkit/glue/webview.h" - -using WebKit::WebScriptSource; -using WebKit::WebString; - -namespace { - -const int32 kRouteId = 5; -const int32 kOpenerId = 7; - -}; - -class RenderViewTest : public testing::Test { - public: - RenderViewTest() {} - ~RenderViewTest() {} - - protected: - // Spins the message loop to process all messages that are currently pending. - void ProcessPendingMessages() { - msg_loop_.PostTask(FROM_HERE, new MessageLoop::QuitTask()); - msg_loop_.Run(); - } - - // Returns a pointer to the main frame. - WebFrame* GetMainFrame() { - return view_->webview()->GetMainFrame(); - } - - // Executes the given JavaScript in the context of the main frame. The input - // is a NULL-terminated UTF-8 string. - void ExecuteJavaScript(const char* js) { - GetMainFrame()->ExecuteScript(WebScriptSource(WebString::fromUTF8(js))); - } - - // Loads the given HTML into the main frame as a data: URL. - void LoadHTML(const char* html) { - std::string url_str = "data:text/html;charset=utf-8,"; - url_str.append(html); - GURL url(url_str); - - scoped_ptr<WebRequest> request(WebRequest::Create(url)); - GetMainFrame()->LoadRequest(request.get()); - - // The load actually happens asynchronously, so we pump messages to process - // the pending continuation. - ProcessPendingMessages(); - } - - // testing::Test - virtual void SetUp() { - WebKit::initialize(&webkitclient_); - WebKit::registerExtension(BaseJsV8Extension::Get()); - WebKit::registerExtension(JsonJsV8Extension::Get()); - WebKit::registerExtension(EventBindings::Get()); - WebKit::registerExtension(RendererExtensionBindings::Get(&render_thread_)); - - mock_process_.reset(new MockProcess()); - - render_thread_.set_routing_id(kRouteId); - - // This needs to pass the mock render thread to the view. - view_ = RenderView::Create(&render_thread_, NULL, NULL, kOpenerId, - WebPreferences(), - new SharedRenderViewCounter(0), kRouteId); - } - virtual void TearDown() { - render_thread_.SendCloseMessage(); - - // Run the loop so the release task from the renderwidget executes. - ProcessPendingMessages(); - - view_ = NULL; - - mock_process_.reset(); - WebKit::shutdown(); - - msg_loop_.RunAllPending(); - } - - MessageLoop msg_loop_; - MockRenderThread render_thread_; - scoped_ptr<MockProcess> mock_process_; - scoped_refptr<RenderView> view_; - RendererWebKitClientImpl webkitclient_; -}; - TEST_F(RenderViewTest, OnLoadAlternateHTMLText) { // Test a new navigation. @@ -374,90 +276,3 @@ TEST_F(RenderViewTest, OnSetTextDirection) { EXPECT_EQ(output, kTextDirection[i].expected_result); } } - -// Tests that the bindings for opening a channel to an extension and sending -// and receiving messages through that channel all works. -TEST_F(RenderViewTest, ExtensionMessagesOpenChannel) { - render_thread_.sink().ClearMessages(); - LoadHTML("<body></body>"); - ExecuteJavaScript( - "var e = new chromium.Extension('foobar');" - "var port = e.connect();" - "port.onmessage.addListener(doOnMessage);" - "port.postMessage({message: 'content ready'});" - "function doOnMessage(msg, port) {" - " alert('content got: ' + msg.val);" - "}"); - - // Verify that we opened a channel and sent a message through it. - const IPC::Message* open_channel_msg = - render_thread_.sink().GetUniqueMessageMatching( - ViewHostMsg_OpenChannelToExtension::ID); - EXPECT_TRUE(open_channel_msg); - - const IPC::Message* post_msg = - render_thread_.sink().GetUniqueMessageMatching( - ViewHostMsg_ExtensionPostMessage::ID); - ASSERT_TRUE(post_msg); - ViewHostMsg_ExtensionPostMessage::Param post_params; - ViewHostMsg_ExtensionPostMessage::Read(post_msg, &post_params); - EXPECT_EQ("{\"message\":\"content ready\"}", post_params.b); - - // Now simulate getting a message back from the other side. - render_thread_.sink().ClearMessages(); - const int kPortId = 0; - RendererExtensionBindings::HandleMessage("{\"val\": 42}", kPortId); - - // Verify that we got it. - const IPC::Message* alert_msg = - render_thread_.sink().GetUniqueMessageMatching( - ViewHostMsg_RunJavaScriptMessage::ID); - ASSERT_TRUE(alert_msg); - void* iter = IPC::SyncMessage::GetDataIterator(alert_msg); - ViewHostMsg_RunJavaScriptMessage::SendParam alert_param; - IPC::ReadParam(alert_msg, &iter, &alert_param); - EXPECT_EQ(L"content got: 42", alert_param.a); -} - -// Tests that the bindings for handling a new channel connection and sending -// and receiving messages through that channel all works. -TEST_F(RenderViewTest, ExtensionMessagesOnConnect) { - LoadHTML("<body></body>"); - ExecuteJavaScript( - "chromium.onconnect.addListener(function (port) {" - " port.onmessage.addListener(doOnMessage);" - " port.postMessage({message: 'onconnect'});" - "});" - "function doOnMessage(msg, port) {" - " alert('got: ' + msg.val);" - "}"); - - render_thread_.sink().ClearMessages(); - - // Simulate a new connection being opened. - const int kPortId = 0; - RendererExtensionBindings::HandleConnect(kPortId); - - // Verify that we handled the new connection by posting a message. - const IPC::Message* post_msg = - render_thread_.sink().GetUniqueMessageMatching( - ViewHostMsg_ExtensionPostMessage::ID); - ASSERT_TRUE(post_msg); - ViewHostMsg_ExtensionPostMessage::Param post_params; - ViewHostMsg_ExtensionPostMessage::Read(post_msg, &post_params); - EXPECT_EQ("{\"message\":\"onconnect\"}", post_params.b); - - // Now simulate getting a message back from the channel opener. - render_thread_.sink().ClearMessages(); - RendererExtensionBindings::HandleMessage("{\"val\": 42}", kPortId); - - // Verify that we got it. - const IPC::Message* alert_msg = - render_thread_.sink().GetUniqueMessageMatching( - ViewHostMsg_RunJavaScriptMessage::ID); - ASSERT_TRUE(alert_msg); - void* iter = IPC::SyncMessage::GetDataIterator(alert_msg); - ViewHostMsg_RunJavaScriptMessage::SendParam alert_param; - IPC::ReadParam(alert_msg, &iter, &alert_param); - EXPECT_EQ(L"got: 42", alert_param.a); -} diff --git a/chrome/renderer/renderer_resources.grd b/chrome/renderer/renderer_resources.grd index 5589cc0..e7084d5 100755 --- a/chrome/renderer/renderer_resources.grd +++ b/chrome/renderer/renderer_resources.grd @@ -9,13 +9,14 @@ </outputs> <release seq="1"> <includes> - <include name="IDR_NET_ERROR_HTML" file="resources\neterror.html" type="BINDATA" /> - <include name="IDR_INSECURE_CONTENT_STAMP" file="resources\insecure_content_stamp.png" type="BINDATA" /> <include name="IDR_ERROR_NO_DETAILS_HTML" file="resources\error_no_details.html" type="BINDATA" /> - <include name="IDR_GREASEMONKEY_API_JS" file="resources\greasemonkey_api.js" type="BINDATA" /> + <include name="IDR_EVENT_BINDINGS_JS" file="resources\event_bindings.js" type="BINDATA" /> <include name="IDR_EXTENSION_PROCESS_BINDINGS_JS" file="resources\extension_process_bindings.js" type="BINDATA" /> + <include name="IDR_GREASEMONKEY_API_JS" file="resources\greasemonkey_api.js" type="BINDATA" /> + <include name="IDR_INSECURE_CONTENT_STAMP" file="resources\insecure_content_stamp.png" type="BINDATA" /> + <include name="IDR_JSON_SCHEMA_JS" file="resources\json_schema.js" type="BINDATA" /> + <include name="IDR_NET_ERROR_HTML" file="resources\neterror.html" type="BINDATA" /> <include name="IDR_RENDERER_EXTENSION_BINDINGS_JS" file="resources\renderer_extension_bindings.js" type="BINDATA" /> - <include name="IDR_EVENT_BINDINGS_JS" file="resources\event_bindings.js" type="BINDATA" /> </includes> </release> </grit>
\ No newline at end of file diff --git a/chrome/renderer/resources/extension_process_bindings.js b/chrome/renderer/resources/extension_process_bindings.js index 3a18c97..ff7ac7e 100644 --- a/chrome/renderer/resources/extension_process_bindings.js +++ b/chrome/renderer/resources/extension_process_bindings.js @@ -1,8 +1,46 @@ var chromium; (function() { + native function GetNextCallbackId(); + native function GetTabsForWindow(); + native function GetTab(); + native function CreateTab(); + native function UpdateTab(); + native function RemoveTab(); + if (!chromium) chromium = {}; + // Validate arguments. + function validate(inst, schemas) { + if (inst.length > schemas.length) + throw new Error("Too many arguments."); + + for (var i = 0; i < schemas.length; i++) { + if (inst[i]) { + var validator = new chromium.JSONSchemaValidator(); + validator.validate(inst[i], schemas[i]); + if (validator.errors.length == 0) + continue; + + var message = "Invalid value for argument " + i + ". "; + for (var i = 0, err; err = validator.errors[i]; i++) { + if (err.path) { + message += "Property '" + err.path + "': "; + } + message += err.message; + message = message.substring(0, message.length - 1); + message += ", "; + } + message = message.substring(0, message.length - 2); + message += "."; + + throw new Error(message); + } else if (!schemas[i].optional) { + throw new Error("Argument " + i + " is required."); + } + } + } + // callback handling var callbacks = []; chromium._dispatchCallback = function(callbackId, str) { @@ -22,7 +60,6 @@ var chromium; var sargs = goog.json.serialize(args); var callbackId = -1; if (callback) { - native function GetNextCallbackId(); callbackId = GetNextCallbackId(); callbacks[callbackId] = callback; } @@ -33,23 +70,61 @@ var chromium; chromium.tabs = {}; // TODO(aa): This should eventually take an optional windowId param. chromium.tabs.getTabsForWindow = function(callback) { - native function GetTabsForWindow(); + validate(arguments, arguments.callee.params); sendRequest(GetTabsForWindow, null, callback); }; + chromium.tabs.getTabsForWindow.params = [ + chromium.types.optFun + ]; + chromium.tabs.getTab = function(tabId, callback) { - native function GetTab(); + validate(arguments, arguments.callee.params); sendRequest(GetTab, tabId, callback); }; + chromium.tabs.getTab.params = [ + chromium.types.pInt, + chromium.types.optFun + ]; + chromium.tabs.createTab = function(tab, callback) { - native function CreateTab(); + validate(arguments, arguments.callee.params); sendRequest(CreateTab, tab, callback); }; + chromium.tabs.createTab.params = [ + { + type: "object", + properties: { + windowId: chromium.types.optPInt, + url: chromium.types.optStr, + selected: chromium.types.optBool + }, + additionalProperties: false + }, + chromium.types.optFun + ]; + chromium.tabs.updateTab = function(tab) { - native function UpdateTab(); + validate(arguments, arguments.callee.params); sendRequest(UpdateTab, tab); }; + chromium.tabs.updateTab.params = [ + { + type: "object", + properties: { + id: chromium.types.pInt, + windowId: chromium.types.optPInt, + url: chromium.types.optStr, + selected: chromium.types.optBool + }, + additionalProperties: false + } + ]; + chromium.tabs.removeTab = function(tabId) { - native function RemoveTab(); + validate(arguments, arguments.callee.params); sendRequest(RemoveTab, tabId); }; + chromium.tabs.removeTab.params = [ + chromium.types.pInt + ]; })(); diff --git a/chrome/renderer/resources/json_schema.js b/chrome/renderer/resources/json_schema.js new file mode 100755 index 0000000..f2d14fb --- /dev/null +++ b/chrome/renderer/resources/json_schema.js @@ -0,0 +1,328 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +//============================================================================== +// This file contains a class that implements a subset of JSON Schema. +// See: http://www.json.com/json-schema-proposal/ for more details. +// +// The following features of JSON Schema are not implemented: +// - requires +// - unique +// - disallow +// - union types +// +// The following properties are not applicable to the interface exposed by +// this class: +// - options +// - readonly +// - title +// - description +// - format +// - default +// - transient +// - hidden +//============================================================================== + +var chromium = chromium || {}; + +/** + * Validates an instance against a schema and accumulates errors. Usage: + * + * var validator = new chromium.JSONSchemaValidator(); + * validator.validate(inst, schema); + * if (validator.errors.length == 0) + * console.log("Valid!"); + * else + * console.log(validator.errors); + * + * The errors property contains a list of objects. Each object has two + * properties: "path" and "message". The "path" property contains the path to + * the key that had the problem, and the "message" property contains a sentence + * describing the error. + */ +chromium.JSONSchemaValidator = function() { + this.errors = []; +}; + +chromium.JSONSchemaValidator.messages = { + invalidEnum: "Value must be one of: [*].", + propertyRequired: "Property is required.", + unexpectedProperty: "Unexpected property.", + arrayMinItems: "Array must have at least * items.", + arrayMaxItems: "Array must not have more than * items.", + itemRequired: "Item is required.", + stringMinLength: "String must be at least * characters long.", + stringMaxLength: "String must not be more than * characters long.", + stringPattern: "String must match the pattern: *.", + numberMinValue: "Value must not be less than *.", + numberMaxValue: "Value must not be greater than *.", + numberMaxDecimal: "Value must not have more than * decimal places.", + invalidType: "Expected '*' but got '*'." +}; + +/** + * Builds an error message. Key is the property in the |errors| object, and + * |opt_replacements| is an array of values to replace "*" characters with. + */ +chromium.JSONSchemaValidator.formatError = function(key, opt_replacements) { + var message = this.messages[key]; + if (opt_replacements) { + for (var i = 0; i < opt_replacements.length; i++) { + message = message.replace("*", opt_replacements[i]); + } + } + return message; +}; + +/** + * Classifies a value as one of the JSON schema primitive types. Note that we + * don't explicitly disallow 'function', because we want to allow functions in + * the input values. + */ +chromium.JSONSchemaValidator.getType = function(value) { + var s = typeof value; + + if (s == "object") { + if (value === null) { + return "null"; + } else if (value instanceof Array || + Object.prototype.toString.call(value) == "[Object Array]") { + return "array"; + } + } else if (s == "number") { + if (value % 1 == 0) { + return "integer"; + } + } + + return s; +}; + +/** + * Validates an instance against a schema. The instance can be any JavaScript + * value and will be validated recursively. When this method returns, the + * |errors| property will contain a list of errors, if any. + */ +chromium.JSONSchemaValidator.prototype.validate = function(instance, schema, + opt_path) { + var path = opt_path || ""; + + // If the schema has an extends property, the instance must validate against + // that schema too. + if (schema.extends) + this.validate(instance, schema.extends, path); + + // If the schema has an enum property, the instance must be one of those + // values. + if (schema.enum) { + if (!this.validateEnum(instance, schema, path)) + return; + } + + if (schema.type && schema.type != "any") { + if (!this.validateType(instance, schema, path)) + return; + + // Type-specific validation. + switch (schema.type) { + case "object": + this.validateObject(instance, schema, path); + break; + case "array": + this.validateArray(instance, schema, path); + break; + case "string": + this.validateString(instance, schema, path); + break; + case "number": + case "integer": + this.validateNumber(instance, schema, path); + break; + } + } +}; + +/** + * Validates an instance against a schema with an enum type. Populates the + * |errors| property, and returns a boolean indicating whether the instance + * validates. + */ +chromium.JSONSchemaValidator.prototype.validateEnum = function(instance, schema, + path) { + for (var i = 0; i < schema.enum.length; i++) { + if (instance === schema.enum[i]) + return true; + } + + this.addError(path, "invalidEnum", [schema.enum.join(", ")]); + return false; +}; + +/** + * Validates an instance against an object schema and populates the errors + * property. + */ +chromium.JSONSchemaValidator.prototype.validateObject = function(instance, + schema, path) { + for (var prop in schema.properties) { + var propPath = path ? path + "." + prop : prop; + if (instance.hasOwnProperty(prop)) { + this.validate(instance[prop], schema.properties[prop], propPath); + } else if (!schema.properties[prop].optional) { + this.addError(propPath, "propertyRequired"); + } + } + + // The additionalProperties property can either be |false| or a schema + // definition. If |false|, additional properties are not allowed. If a schema + // defintion, all additional properties must validate against that schema. + if (typeof schema.additionalProperties != "undefined") { + for (var prop in instance) { + if (instance.hasOwnProperty(prop)) { + var propPath = path ? path + "." + prop : prop; + if (!schema.properties.hasOwnProperty(prop)) { + if (schema.additionalProperties === false) + this.addError(propPath, "unexpectedProperty"); + else + this.validate(instance[prop], schema.additionalProperties, propPath); + } + } + } + } +}; + +/** + * Validates an instance against an array schema and populates the errors + * property. + */ +chromium.JSONSchemaValidator.prototype.validateArray = function(instance, + schema, path) { + var typeOfItems = chromium.JSONSchemaValidator.getType(schema.items); + + if (typeOfItems == 'object') { + if (schema.minItems && instance.length < schema.minItems) { + this.addError(path, "arrayMinItems", [schema.minItems]); + } + + if (typeof schema.maxItems != "undefined" && + instance.length > schema.maxItems) { + this.addError(path, "arrayMaxItems", [schema.maxItems]); + } + + // If the items property is a single schema, each item in the array must + // have that schema. + for (var i = 0; i < instance.length; i++) { + this.validate(instance[i], schema.items, path + "[" + i + "]"); + } + } else if (typeOfItems == 'array') { + // If the items property is an array of schemas, each item in the array must + // validate against the corresponding schema. + for (var i = 0; i < schema.items.length; i++) { + var itemPath = path ? path + "[" + i + "]" : String(i); + if (instance.hasOwnProperty(i)) { + this.validate(instance[i], schema.items[i], itemPath); + } else if (!schema.items[i].optional) { + this.addError(itemPath, "itemRequired"); + } + } + + if (schema.additionalProperties === false) { + if (instance.length > schema.items.length) { + this.addError(path, "arrayMaxItems", [schema.items.length]); + } + } else if (schema.additionalProperties) { + for (var i = schema.items.length; i < instance.length; i++) { + var itemPath = path ? path + "[" + i + "]" : String(i); + this.validate(instance[i], schema.additionalProperties, itemPath); + } + } + } +}; + +/** + * Validates a string and populates the errors property. + */ +chromium.JSONSchemaValidator.prototype.validateString = function(instance, + schema, path) { + if (schema.minLength && instance.length < schema.minLength) + this.addError(path, "stringMinLength", [schema.minLength]); + + if (schema.maxLength && instance.length > schema.maxLength) + this.addError(path, "stringMaxLength", [schema.maxLength]); + + if (schema.pattern && !schema.pattern.test(instance)) + this.addError(path, "stringPattern", [schema.pattern]); +}; + +/** + * Validates a number and populates the errors property. The instance is + * assumed to be a number. + */ +chromium.JSONSchemaValidator.prototype.validateNumber = function(instance, + schema, path) { + if (schema.minimum && instance < schema.minimum) + this.addError(path, "numberMinValue", [schema.minimum]); + + if (schema.maximum && instance > schema.maximum) + this.addError(path, "numberMaxValue", [schema.maximum]); + + if (schema.maxDecimal && instance * Math.pow(10, schema.maxDecimal) % 1) + this.addError(path, "numberMaxDecimal", [schema.maxDecimal]); +}; + +/** + * Validates the primitive type of an instance and populates the errors + * property. Returns true if the instance validates, false otherwise. + */ +chromium.JSONSchemaValidator.prototype.validateType = function(instance, schema, + path) { + var actualType = chromium.JSONSchemaValidator.getType(instance); + if (schema.type != actualType && !(schema.type == "number" && + actualType == "integer")) { + this.addError(path, "invalidType", [schema.type, actualType]); + return false; + } + + return true; +}; + +/** + * Adds an error message. |key| is an index into the |messages| object. + * |replacements| is an array of values to replace '*' characters in the + * message. + */ +chromium.JSONSchemaValidator.prototype.addError = function(path, key, + replacements) { + this.errors.push({ + path: path, + message: chromium.JSONSchemaValidator.formatError(key, replacements) + }); +}; + +// Set up chromium.types with some commonly used types... +(function() { + function extend(base, ext) { + var result = {}; + for (var p in base) + result[p] = base[p]; + for (var p in ext) + result[p] = ext[p]; + return result; + } + + var types = {}; + types.opt = {optional: true}; + types.bool = {type: "boolean"}; + types.int = {type: "integer"}; + types.str = {type: "string"}; + types.fun = {type: "function"}; + types.pInt = extend(types.int, {minimum: 0}); + types.optBool = extend(types.bool, types.opt); + types.optInt = extend(types.int, types.opt); + types.optStr = extend(types.str, types.opt); + types.optFun = extend(types.fun, types.opt); + types.optPInt = extend(types.pInt, types.opt); + + chromium.types = types; +})(); diff --git a/chrome/test/data/extensions/json_schema_test.js b/chrome/test/data/extensions/json_schema_test.js new file mode 100755 index 0000000..497c344 --- /dev/null +++ b/chrome/test/data/extensions/json_schema_test.js @@ -0,0 +1,314 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +function assert(truth) { + if (!truth) + throw new Error("Assertion failed."); +} + +function assertValid(type, instance, schema) { + var validator = new chromium.JSONSchemaValidator(); + validator["validate" + type](instance, schema, ""); + if (validator.errors.length != 0) { + console.log("Got unexpected errors"); + console.log(validator.errors); + assert(false); + } +} + +function assertNotValid(type, instance, schema, errors) { + var validator = new chromium.JSONSchemaValidator(); + validator["validate" + type](instance, schema, ""); + assert(validator.errors.length === errors.length); + for (var i = 0; i < errors.length; i++) { + if (validator.errors[i].message == errors[i]) { + log("Got expected error: " + validator.errors[i].message + " for path: " + + validator.errors[i].path); + } else { + log("Missed expected error: " + errors[i] + ". Got: " + + validator.errors[i].message + " instead."); + assert(false); + } + } +} + +function formatError(key, replacements) { + return chromium.JSONSchemaValidator.formatError(key, replacements); +} + +function testFormatError() { + assert(formatError("propertyRequired") == "Property is required."); + assert(formatError("invalidEnum", ["foo, bar"]) == + "Value must be one of: [foo, bar]."); + assert(formatError("invalidType", ["foo", "bar"]) == + "Expected 'foo' but got 'bar'."); +} + +function testComplex() { + var schema = { + type: "array", + items: [ + { + type: "object", + properties: { + id: { + type: "integer", + minimum: 1 + }, + url: { + type: "string", + pattern: /^https?\:/, + optional: true + }, + index: { + type: "integer", + minimum: 0, + optional: true + }, + selected: { + type: "boolean", + optional: true + } + } + }, + { type: "function", optional: true }, + { type: "function", optional: true } + ] + }; + + var instance = [ + { + id: 42, + url: "http://www.google.com/", + index: 2, + selected: true + }, + function(){}, + function(){} + ]; + + assertValid("", instance, schema); + instance.length = 2; + assertValid("", instance, schema); + instance.length = 1; + instance.push({}); + assertNotValid("", instance, schema, + [formatError("invalidType", ["function", "object"])]); + instance[1] = function(){}; + + instance[0].url = "foo"; + assertNotValid("", instance, schema, + [formatError("stringPattern", + [schema.items[0].properties.url.pattern])]); + delete instance[0].url; + assertValid("", instance, schema); + + instance[0].id = 0; + assertNotValid("", instance, schema, + [formatError("numberMinValue", + [schema.items[0].properties.id.minimum])]); +} + +function testEnum() { + var schema = { + enum: ["foo", 42, false] + }; + + assertValid("", "foo", schema); + assertValid("", 42, schema); + assertValid("", false, schema); + assertNotValid("", "42", schema, [formatError("invalidEnum", + [schema.enum.join(", ")])]); + assertNotValid("", null, schema, [formatError("invalidEnum", + [schema.enum.join(", ")])]); +} + +function testExtends() { + var parent = { + type: "number" + } + var schema = { + extends: parent + }; + + assertValid("", 42, schema); + assertNotValid("", "42", schema, + [formatError("invalidType", ["number", "string"])]); + + // Make the derived schema more restrictive + parent.minimum = 43; + assertNotValid("", 42, schema, [formatError("numberMinValue", [43])]); + assertValid("", 43, schema); +} + +function testObject() { + var schema = { + properties: { + "foo": { + type: "string" + }, + "bar": { + type: "integer" + } + } + }; + + assertValid("Object", {foo:"foo",bar:42}, schema); + assertValid("Object", {foo:"foo",bar:42,"extra":true}, schema); + assertNotValid("Object", {foo:"foo"}, schema, + [formatError("propertyRequired")]); + assertNotValid("Object", {foo:"foo", bar:"42"}, schema, + [formatError("invalidType", ["integer", "string"])]); + + schema.additionalProperties = false; + assertNotValid("Object", {foo:"foo",bar:42,"extra":true}, schema, + [formatError("unexpectedProperty")]); + + schema.additionalProperties = { + type: "boolean" + }; + assertValid("Object", {foo:"foo",bar:42,"extra":true}, schema); + + schema.properties.bar.optional = true; + assertValid("Object", {foo:"foo",bar:42}, schema); + assertValid("Object", {foo:"foo"}, schema); + assertNotValid("Object", {foo:"foo", bar:"42"}, schema, + [formatError("invalidType", ["integer", "string"])]); +} + +function testArrayTuple() { + var schema = { + items: [ + { + type: "string" + }, + { + type: "integer" + } + ] + }; + + assertValid("Array", ["42", 42], schema); + assertValid("Array", ["42", 42, "anything"], schema); + assertNotValid("Array", ["42"], schema, [formatError("itemRequired")]); + assertNotValid("Array", [42, 42], schema, + [formatError("invalidType", ["string", "integer"])]); + + schema.additionalProperties = false; + assertNotValid("Array", ["42", 42, "anything"], schema, + [formatError("arrayMaxItems", [schema.items.length])]); + + schema.additionalProperties = { + type: "boolean" + }; + assertNotValid("Array", ["42", 42, "anything"], schema, + [formatError("invalidType", ["boolean", "string"])]); + assertValid("Array", ["42", 42, false], schema); + + schema.items[0].optional = true; + assertValid("Array", ["42", 42], schema); + assertValid("Array", [, 42], schema); + assertNotValid("Array", [42, 42], schema, + [formatError("invalidType", ["string", "integer"])]); +} + +function testArrayNonTuple() { + var schema = { + items: { + type: "string" + }, + minItems: 2, + maxItems: 3 + }; + + assertValid("Array", ["x", "x"], schema); + assertValid("Array", ["x", "x", "x"], schema); + + assertNotValid("Array", ["x"], schema, + [formatError("arrayMinItems", [schema.minItems])]); + assertNotValid("Array", ["x", "x", "x", "x"], schema, + [formatError("arrayMaxItems", [schema.maxItems])]); + assertNotValid("Array", [42], schema, + [formatError("arrayMinItems", [schema.minItems]), + formatError("invalidType", ["string", "integer"])]); +} + +function testString() { + var schema = { + minLength: 1, + maxLength: 10, + pattern: /^x/ + }; + + assertValid("String", "x", schema); + assertValid("String", "xxxxxxxxxx", schema); + + assertNotValid("String", "y", schema, + [formatError("stringPattern", [schema.pattern])]); + assertNotValid("String", "xxxxxxxxxxx", schema, + [formatError("stringMaxLength", [schema.maxLength])]); + assertNotValid("String", "", schema, + [formatError("stringMinLength", [schema.minLength]), + formatError("stringPattern", [schema.pattern])]); +} + +function testNumber() { + var schema = { + minimum: 1, + maximum: 100, + maxDecimal: 2 + }; + + assertValid("Number", 1, schema); + assertValid("Number", 50, schema); + assertValid("Number", 100, schema); + assertValid("Number", 88.88, schema); + + assertNotValid("Number", 0.5, schema, + [formatError("numberMinValue", [schema.minimum])]); + assertNotValid("Number", 100.1, schema, + [formatError("numberMaxValue", [schema.maximum])]); + assertNotValid("Number", 100.111, schema, + [formatError("numberMaxValue", [schema.maximum]), + formatError("numberMaxDecimal", [schema.maxDecimal])]); +} + +function testType() { + // valid + assertValid("Type", {}, {type:"object"}); + assertValid("Type", [], {type:"array"}); + assertValid("Type", function(){}, {type:"function"}); + assertValid("Type", "foobar", {type:"string"}); + assertValid("Type", "", {type:"string"}); + assertValid("Type", 88.8, {type:"number"}); + assertValid("Type", 42, {type:"number"}); + assertValid("Type", 0, {type:"number"}); + assertValid("Type", 42, {type:"integer"}); + assertValid("Type", 0, {type:"integer"}); + assertValid("Type", true, {type:"boolean"}); + assertValid("Type", false, {type:"boolean"}); + assertValid("Type", null, {type:"null"}); + + // not valid + assertNotValid("Type", [], {type: "object"}, + [formatError("invalidType", ["object", "array"])]); + assertNotValid("Type", null, {type: "object"}, + [formatError("invalidType", ["object", "null"])]); + assertNotValid("Type", function(){}, {type: "object"}, + [formatError("invalidType", ["object", "function"])]); + assertNotValid("Type", 42, {type: "array"}, + [formatError("invalidType", ["array", "integer"])]); + assertNotValid("Type", 42, {type: "string"}, + [formatError("invalidType", ["string", "integer"])]); + assertNotValid("Type", "42", {type: "number"}, + [formatError("invalidType", ["number", "string"])]); + assertNotValid("Type", 88.8, {type: "integer"}, + [formatError("invalidType", ["integer", "number"])]); + assertNotValid("Type", 1, {type: "boolean"}, + [formatError("invalidType", ["boolean", "integer"])]); + assertNotValid("Type", false, {type: "null"}, + [formatError("invalidType", ["null", "boolean"])]); + assertNotValid("Type", {}, {type: "function"}, + [formatError("invalidType", ["function", "object"])]); +} diff --git a/chrome/test/data/js_test_runner.html b/chrome/test/data/js_test_runner.html new file mode 100755 index 0000000..55d0448 --- /dev/null +++ b/chrome/test/data/js_test_runner.html @@ -0,0 +1,53 @@ +<!--
+Copyright (c) 2009 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. + +This file is a manual test runner for JavaScript unit tests. It finds all the
+global functions starting with "test" and runs them. It is useful for developing
+and debugging JavaScript unit tests.
+
+See: chrome/test/data/extensions/schema_test.js for an example.
+-->
+<textarea style="position:absolute; left:5px; top:5px; right:5px; bottom:5px;">
+</textarea>
+
+<!-- Add a reference to the script and the script test files here. -->
+<script src="../../../renderer/resources/schema.js"></script>
+<script src="schema_test.js"></script>
+
+<script>
+function log() {
+ console.log.apply(console, arguments);
+}
+
+function runAllTests() {
+ for (var p in window) {
+ if (p.substring(0, 4) == "test") {
+ runTest(p);
+ }
+ }
+ window.setTimeout(function() {
+ log("DONE");
+ }, 0);
+}
+
+function runTest(p) {
+ window.setTimeout(function() {
+ var success = false;
+ try {
+ window[p]();
+ success = true;
+ } finally {
+ print((success ? "PASS" : "FAIL") + " " + p);
+ }
+ }, 0);
+}
+
+function print(msg) {
+ document.getElementsByTagName("textarea")[0].value += msg + "\n";
+}
+
+runAllTests();
+
+</script>
diff --git a/chrome/test/render_view_test.cc b/chrome/test/render_view_test.cc new file mode 100644 index 0000000..49d4c63 --- /dev/null +++ b/chrome/test/render_view_test.cc @@ -0,0 +1,89 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/test/render_view_test.h" + +#include "chrome/common/render_messages.h" +#include "chrome/browser/extensions/extension_function_dispatcher.h" +#include "chrome/renderer/extensions/event_bindings.h" +#include "chrome/renderer/extensions/extension_process_bindings.h" +#include "chrome/renderer/extensions/renderer_extension_bindings.h" +#include "chrome/renderer/js_only_v8_extensions.h" +#include "third_party/WebKit/WebKit/chromium/public/WebKit.h" +#include "third_party/WebKit/WebKit/chromium/public/WebScriptSource.h" +#include "webkit/glue/weburlrequest.h" +#include "webkit/glue/webview.h" + +using WebKit::WebScriptSource; +using WebKit::WebString; + +namespace { + +const int32 kRouteId = 5; +const int32 kOpenerId = 7; + +}; + +void RenderViewTest::ProcessPendingMessages() { + msg_loop_.PostTask(FROM_HERE, new MessageLoop::QuitTask()); + msg_loop_.Run(); +} + +WebFrame* RenderViewTest::GetMainFrame() { + return view_->webview()->GetMainFrame(); +} + +void RenderViewTest::ExecuteJavaScript(const char* js) { + GetMainFrame()->ExecuteScript(WebScriptSource(WebString::fromUTF8(js))); +} + +void RenderViewTest::LoadHTML(const char* html) { + std::string url_str = "data:text/html;charset=utf-8,"; + url_str.append(html); + GURL url(url_str); + + scoped_ptr<WebRequest> request(WebRequest::Create(url)); + GetMainFrame()->LoadRequest(request.get()); + + // The load actually happens asynchronously, so we pump messages to process + // the pending continuation. + ProcessPendingMessages(); +} + +void RenderViewTest::SetUp() { + WebKit::initialize(&webkitclient_); + WebKit::registerExtension(BaseJsV8Extension::Get()); + WebKit::registerExtension(JsonJsV8Extension::Get()); + WebKit::registerExtension(JsonSchemaJsV8Extension::Get()); + WebKit::registerExtension(EventBindings::Get()); + WebKit::registerExtension(ExtensionProcessBindings::Get()); + WebKit::registerExtension(RendererExtensionBindings::Get(&render_thread_)); + + // TODO(aa): Should some of this go to some other inheriting class? + std::vector<std::string> names; + ExtensionFunctionDispatcher::GetAllFunctionNames(&names); + ExtensionProcessBindings::SetFunctionNames(names); + + mock_process_.reset(new MockProcess()); + + render_thread_.set_routing_id(kRouteId); + + // This needs to pass the mock render thread to the view. + view_ = RenderView::Create(&render_thread_, NULL, NULL, kOpenerId, + WebPreferences(), + new SharedRenderViewCounter(0), kRouteId); +} +void RenderViewTest::TearDown() { + render_thread_.SendCloseMessage(); + + // Run the loop so the release task from the renderwidget executes. + ProcessPendingMessages(); + + view_ = NULL; + + mock_process_.reset(); + WebKit::shutdown(); + + msg_loop_.RunAllPending(); +} diff --git a/chrome/test/render_view_test.h b/chrome/test/render_view_test.h new file mode 100644 index 0000000..abda898 --- /dev/null +++ b/chrome/test/render_view_test.h @@ -0,0 +1,47 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_TEST_RENDER_VIEW_TEST_H_ +#define CHROME_TEST_RENDER_VIEW_TEST_H_ + +#include "base/scoped_ptr.h" +#include "chrome/renderer/mock_render_process.h" +#include "chrome/renderer/mock_render_thread.h" +#include "chrome/renderer/render_view.h" +#include "chrome/renderer/renderer_webkitclient_impl.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "webkit/glue/webframe.h" + +class RenderViewTest : public testing::Test { + public: + RenderViewTest() {} + ~RenderViewTest() {} + + protected: + // Spins the message loop to process all messages that are currently pending. + void ProcessPendingMessages(); + + // Returns a pointer to the main frame. + WebFrame* GetMainFrame(); + + // Executes the given JavaScript in the context of the main frame. The input + // is a NULL-terminated UTF-8 string. + void ExecuteJavaScript(const char* js); + + // Loads the given HTML into the main frame as a data: URL. + void LoadHTML(const char* html); + + // testing::Test + virtual void SetUp(); + + virtual void TearDown(); + + MessageLoop msg_loop_; + MockRenderThread render_thread_; + scoped_ptr<MockProcess> mock_process_; + scoped_refptr<RenderView> view_; + RendererWebKitClientImpl webkitclient_; +}; + +#endif // CHROME_TEST_RENDER_VIEW_TEST_H_ diff --git a/chrome/test/unit/unittests.vcproj b/chrome/test/unit/unittests.vcproj index 4483461..4a7adb9 100644 --- a/chrome/test/unit/unittests.vcproj +++ b/chrome/test/unit/unittests.vcproj @@ -192,6 +192,14 @@ > </File> <File + RelativePath="..\render_view_test.cc" + > + </File> + <File + RelativePath="..\render_view_test.h" + > + </File> + <File RelativePath=".\run_all_unittests.cc" > </File> @@ -488,6 +496,10 @@ > </File> <File + RelativePath="..\..\browser\extensions\extension_messages_unittest.cc" + > + </File> + <File RelativePath="..\..\browser\extensions\extension_process_manager_unittest.cc" > </File> @@ -844,10 +856,18 @@ Name="renderer" > <File + RelativePath="..\..\renderer\extensions\extension_api_client_unittest.cc" + > + </File> + <File RelativePath="..\..\renderer\extensions\greasemonkey_api_unittest.cc" > </File> <File + RelativePath="..\..\renderer\extensions\json_schema_unittest.cc" + > + </File> + <File RelativePath="..\..\renderer\net\render_dns_master_unittest.cc" > </File> |