blob: df21db24d6077239297054c916863c74585a248f [file] [log] [blame]
// Copyright 2017 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 "extensions/renderer/bindings/api_binding_js_util.h"
#include "base/bind.h"
#include "extensions/renderer/bindings/api_binding_test_util.h"
#include "extensions/renderer/bindings/api_bindings_system.h"
#include "extensions/renderer/bindings/api_bindings_system_unittest.h"
#include "gin/arguments.h"
#include "gin/handle.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace extensions {
namespace {
// Calls handleException on |obj|, which is presumed to be the JS binding util.
const char kHandleException[] =
"try {\n"
" throw new Error('some error');\n"
"} catch (e) {\n"
" obj.handleException('handled', e);\n"
"}";
} // namespace
class APIBindingJSUtilUnittest : public APIBindingsSystemTest {
protected:
APIBindingJSUtilUnittest() {}
~APIBindingJSUtilUnittest() override {}
gin::Handle<APIBindingJSUtil> CreateUtil() {
return gin::CreateHandle(
isolate(),
new APIBindingJSUtil(bindings_system()->type_reference_map(),
bindings_system()->request_handler(),
bindings_system()->event_handler(),
bindings_system()->exception_handler(),
base::Bind(&RunFunctionOnGlobalAndIgnoreResult)));
}
v8::Local<v8::Object> GetLastErrorParent(
v8::Local<v8::Context> context,
v8::Local<v8::Object>* secondary_parent) override {
return context->Global();
}
void AddConsoleError(v8::Local<v8::Context> context,
const std::string& error) override {
console_errors_.push_back(error);
}
std::string GetExposedError(v8::Local<v8::Context> context) {
v8::Local<v8::Value> last_error =
GetPropertyFromObject(context->Global(), context, "lastError");
// Use ADD_FAILURE() to avoid messing up the return type with ASSERT.
if (last_error.IsEmpty()) {
ADD_FAILURE();
return std::string();
}
if (!last_error->IsObject() && !last_error->IsUndefined()) {
ADD_FAILURE();
return std::string();
}
if (last_error->IsUndefined())
return "[undefined]";
return GetStringPropertyFromObject(last_error.As<v8::Object>(), context,
"message");
}
APILastError* last_error() {
return bindings_system()->request_handler()->last_error();
}
const std::vector<std::string>& console_errors() const {
return console_errors_;
}
private:
std::vector<std::string> console_errors_;
DISALLOW_COPY_AND_ASSIGN(APIBindingJSUtilUnittest);
};
TEST_F(APIBindingJSUtilUnittest, TestSetLastError) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
gin::Handle<APIBindingJSUtil> util = CreateUtil();
v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
EXPECT_FALSE(last_error()->HasError(context));
EXPECT_EQ("[undefined]", GetExposedError(context));
const char kSetLastError[] = "obj.setLastError('a last error');";
CallFunctionOnObject(context, v8_util, kSetLastError);
EXPECT_TRUE(last_error()->HasError(context));
EXPECT_EQ("\"a last error\"", GetExposedError(context));
CallFunctionOnObject(context, v8_util,
"obj.setLastError('a new last error')");
EXPECT_TRUE(last_error()->HasError(context));
EXPECT_EQ("\"a new last error\"", GetExposedError(context));
}
TEST_F(APIBindingJSUtilUnittest, TestHasLastError) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
gin::Handle<APIBindingJSUtil> util = CreateUtil();
v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
EXPECT_FALSE(last_error()->HasError(context));
EXPECT_EQ("[undefined]", GetExposedError(context));
const char kHasLastError[] = "return obj.hasLastError();";
v8::Local<v8::Value> has_error =
CallFunctionOnObject(context, v8_util, kHasLastError);
EXPECT_EQ("false", V8ToString(has_error, context));
last_error()->SetError(context, "an error");
EXPECT_TRUE(last_error()->HasError(context));
EXPECT_EQ("\"an error\"", GetExposedError(context));
has_error = CallFunctionOnObject(context, v8_util, kHasLastError);
EXPECT_EQ("true", V8ToString(has_error, context));
last_error()->ClearError(context, false);
EXPECT_FALSE(last_error()->HasError(context));
EXPECT_EQ("[undefined]", GetExposedError(context));
has_error = CallFunctionOnObject(context, v8_util, kHasLastError);
EXPECT_EQ("false", V8ToString(has_error, context));
}
TEST_F(APIBindingJSUtilUnittest, TestRunWithLastError) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
gin::Handle<APIBindingJSUtil> util = CreateUtil();
v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
EXPECT_FALSE(last_error()->HasError(context));
EXPECT_EQ("[undefined]", GetExposedError(context));
const char kRunWithLastError[] =
"obj.runCallbackWithLastError('last error', function() {\n"
" this.exposedLastError =\n"
" this.lastError ? this.lastError.message : 'undefined';\n"
"}, [1, 'foo']);";
CallFunctionOnObject(context, v8_util, kRunWithLastError);
EXPECT_FALSE(last_error()->HasError(context));
EXPECT_EQ("[undefined]", GetExposedError(context));
EXPECT_EQ("\"last error\"",
GetStringPropertyFromObject(context->Global(), context,
"exposedLastError"));
}
TEST_F(APIBindingJSUtilUnittest, TestSendRequestWithOptions) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
gin::Handle<APIBindingJSUtil> util = CreateUtil();
v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
const char kSendRequestWithNoOptions[] =
"obj.sendRequest('alpha.functionWithCallback',\n"
" ['someString', function() {}], undefined, undefined);";
CallFunctionOnObject(context, v8_util, kSendRequestWithNoOptions);
ASSERT_TRUE(last_request());
EXPECT_EQ("alpha.functionWithCallback", last_request()->method_name);
EXPECT_EQ("[\"someString\"]", ValueToString(*last_request()->arguments));
EXPECT_EQ(binding::RequestThread::UI, last_request()->thread);
reset_last_request();
const char kSendRequestForIOThread[] =
"obj.sendRequest('alpha.functionWithCallback',\n"
" ['someOtherString', function() {}], undefined,\n"
" {__proto__: null, forIOThread: true});";
CallFunctionOnObject(context, v8_util, kSendRequestForIOThread);
ASSERT_TRUE(last_request());
EXPECT_EQ("alpha.functionWithCallback", last_request()->method_name);
EXPECT_EQ("[\"someOtherString\"]", ValueToString(*last_request()->arguments));
EXPECT_EQ(binding::RequestThread::IO, last_request()->thread);
reset_last_request();
const char kSendRequestForUIThread[] =
"obj.sendRequest('alpha.functionWithCallback',\n"
" ['someOtherString', function() {}], undefined,\n"
" {__proto__: null, forIOThread: false});";
CallFunctionOnObject(context, v8_util, kSendRequestForUIThread);
ASSERT_TRUE(last_request());
EXPECT_EQ("alpha.functionWithCallback", last_request()->method_name);
EXPECT_EQ("[\"someOtherString\"]", ValueToString(*last_request()->arguments));
EXPECT_EQ(binding::RequestThread::UI, last_request()->thread);
reset_last_request();
const char kSendRequestWithCustomCallback[] =
R"(obj.sendRequest(
'alpha.functionWithCallback',
['stringy', function() {}], undefined,
{
__proto__: null,
customCallback: function() {
this.callbackCalled = true;
},
});)";
CallFunctionOnObject(context, v8_util, kSendRequestWithCustomCallback);
ASSERT_TRUE(last_request());
EXPECT_EQ("alpha.functionWithCallback", last_request()->method_name);
EXPECT_EQ("[\"stringy\"]", ValueToString(*last_request()->arguments));
EXPECT_EQ(binding::RequestThread::UI, last_request()->thread);
bindings_system()->CompleteRequest(last_request()->request_id,
base::ListValue(), std::string());
EXPECT_EQ("true", GetStringPropertyFromObject(context->Global(), context,
"callbackCalled"));
}
TEST_F(APIBindingJSUtilUnittest, TestCallHandleException) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
gin::Handle<APIBindingJSUtil> util = CreateUtil();
v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
ASSERT_TRUE(console_errors().empty());
CallFunctionOnObject(context, v8_util, kHandleException);
EXPECT_THAT(console_errors(),
testing::ElementsAre("handled: Error: some error"));
const char kHandleTrickyException[] =
"try {\n"
" throw { toString: function() { throw new Error('hahaha'); } };\n"
"} catch (e) {\n"
" obj.handleException('handled again', e);\n"
"}\n";
CallFunctionOnObject(context, v8_util, kHandleTrickyException);
EXPECT_THAT(
console_errors(),
testing::ElementsAre("handled: Error: some error",
"handled again: (failed to get error message)"));
}
TEST_F(APIBindingJSUtilUnittest, TestSetExceptionHandler) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
gin::Handle<APIBindingJSUtil> util = CreateUtil();
v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
struct ErrorInfo {
std::string full_message;
std::string exception_message;
};
auto custom_handler = [](const v8::FunctionCallbackInfo<v8::Value>& info) {
gin::Arguments arguments(info);
std::string full_message;
ASSERT_TRUE(arguments.GetNext(&full_message));
v8::Local<v8::Object> error_object;
ASSERT_TRUE(arguments.GetNext(&error_object));
ASSERT_TRUE(info.Data()->IsExternal());
ErrorInfo* error_out =
static_cast<ErrorInfo*>(info.Data().As<v8::External>()->Value());
error_out->full_message = full_message;
error_out->exception_message = GetStringPropertyFromObject(
error_object, arguments.GetHolderCreationContext(), "message");
};
ErrorInfo error_info;
v8::Local<v8::Function> v8_handler =
v8::Function::New(context, custom_handler,
v8::External::New(isolate(), &error_info))
.ToLocalChecked();
v8::Local<v8::Function> add_handler = FunctionFromString(
context,
"(function(util, handler) { util.setExceptionHandler(handler); })");
v8::Local<v8::Value> args[] = {v8_util, v8_handler};
RunFunction(add_handler, context, arraysize(args), args);
CallFunctionOnObject(context, v8_util, kHandleException);
// The error should not have been reported to the console since we have a
// cusotm handler.
EXPECT_TRUE(console_errors().empty());
EXPECT_EQ("handled: Error: some error", error_info.full_message);
EXPECT_EQ("\"some error\"", error_info.exception_message);
}
} // namespace extensions