blob: d799d5559a8ac810516409efb8e72c9f44640553 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "gin/wrappable.h"
#include "base/check.h"
#include "gin/arguments.h"
#include "gin/handle.h"
#include "gin/object_template_builder.h"
#include "gin/per_isolate_data.h"
#include "gin/public/isolate_holder.h"
#include "gin/test/v8_test.h"
#include "gin/try_catch.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "v8/include/v8-function.h"
#include "v8/include/v8-message.h"
#include "v8/include/v8-script.h"
namespace gin {
namespace {
// A non-member function to be bound to an ObjectTemplateBuilder.
void NonMemberMethod() {}
// This useless base class ensures that the value of a pointer to a MyObject
// (below) is not the same as the value of that pointer cast to the object's
// WrappableBase base.
class BaseClass {
public:
BaseClass() : value_(23) {}
BaseClass(const BaseClass&) = delete;
BaseClass& operator=(const BaseClass&) = delete;
virtual ~BaseClass() = default;
// So the compiler doesn't complain that |value_| is unused.
int value() const { return value_; }
private:
int value_;
};
class MyObject : public BaseClass,
public Wrappable<MyObject> {
public:
MyObject(const MyObject&) = delete;
MyObject& operator=(const MyObject&) = delete;
static WrapperInfo kWrapperInfo;
static gin::Handle<MyObject> Create(v8::Isolate* isolate) {
return CreateHandle(isolate, new MyObject());
}
int value() const { return value_; }
void set_value(int value) { value_ = value; }
void Method() {}
protected:
MyObject() : value_(0) {}
ObjectTemplateBuilder GetObjectTemplateBuilder(v8::Isolate* isolate) final {
return Wrappable<MyObject>::GetObjectTemplateBuilder(isolate)
.SetProperty("value", &MyObject::value, &MyObject::set_value)
.SetMethod("memberMethod", &MyObject::Method)
.SetMethod("nonMemberMethod", &NonMemberMethod);
}
~MyObject() override = default;
private:
int value_;
};
class MyObject2 : public Wrappable<MyObject2> {
public:
static WrapperInfo kWrapperInfo;
};
class MyNamedObject : public Wrappable<MyNamedObject> {
public:
MyNamedObject(const MyNamedObject&) = delete;
MyNamedObject& operator=(const MyNamedObject&) = delete;
static WrapperInfo kWrapperInfo;
static gin::Handle<MyNamedObject> Create(v8::Isolate* isolate) {
return CreateHandle(isolate, new MyNamedObject());
}
void Method() {}
protected:
MyNamedObject() = default;
ObjectTemplateBuilder GetObjectTemplateBuilder(v8::Isolate* isolate) final {
return Wrappable<MyNamedObject>::GetObjectTemplateBuilder(isolate)
.SetMethod("memberMethod", &MyNamedObject::Method)
.SetMethod("nonMemberMethod", &NonMemberMethod);
}
const char* GetTypeName() final { return "MyNamedObject"; }
~MyNamedObject() override = default;
};
WrapperInfo MyObject::kWrapperInfo = { kEmbedderNativeGin };
WrapperInfo MyObject2::kWrapperInfo = { kEmbedderNativeGin };
WrapperInfo MyNamedObject::kWrapperInfo = {kEmbedderNativeGin};
} // namespace
typedef V8Test WrappableTest;
TEST_F(WrappableTest, WrapAndUnwrap) {
v8::Isolate* isolate = instance_->isolate();
v8::HandleScope handle_scope(isolate);
Handle<MyObject> obj = MyObject::Create(isolate);
v8::Local<v8::Value> wrapper =
ConvertToV8(isolate, obj.get()).ToLocalChecked();
EXPECT_FALSE(wrapper.IsEmpty());
MyObject* unwrapped = NULL;
EXPECT_TRUE(ConvertFromV8(isolate, wrapper, &unwrapped));
EXPECT_EQ(obj.get(), unwrapped);
}
TEST_F(WrappableTest, UnwrapFailures) {
v8::Isolate* isolate = instance_->isolate();
v8::HandleScope handle_scope(isolate);
// Something that isn't an object.
v8::Local<v8::Value> thing = v8::Number::New(isolate, 42);
MyObject* unwrapped = NULL;
EXPECT_FALSE(ConvertFromV8(isolate, thing, &unwrapped));
EXPECT_FALSE(unwrapped);
// An object that's not wrapping anything.
thing = v8::Object::New(isolate);
EXPECT_FALSE(ConvertFromV8(isolate, thing, &unwrapped));
EXPECT_FALSE(unwrapped);
// An object that's wrapping a C++ object of the wrong type.
thing.Clear();
thing = ConvertToV8(isolate, new MyObject2()).ToLocalChecked();
EXPECT_FALSE(ConvertFromV8(isolate, thing, &unwrapped));
EXPECT_FALSE(unwrapped);
}
TEST_F(WrappableTest, GetAndSetProperty) {
v8::Isolate* isolate = instance_->isolate();
v8::HandleScope handle_scope(isolate);
gin::Handle<MyObject> obj = MyObject::Create(isolate);
obj->set_value(42);
EXPECT_EQ(42, obj->value());
v8::Local<v8::String> source = StringToV8(isolate,
"(function (obj) {"
" if (obj.value !== 42) throw 'FAIL';"
" else obj.value = 191; })");
EXPECT_FALSE(source.IsEmpty());
gin::TryCatch try_catch(isolate);
v8::Local<v8::Script> script =
v8::Script::Compile(context_.Get(isolate), source).ToLocalChecked();
v8::Local<v8::Value> val =
script->Run(context_.Get(isolate)).ToLocalChecked();
v8::Local<v8::Function> func;
EXPECT_TRUE(ConvertFromV8(isolate, val, &func));
v8::Local<v8::Value> argv[] = {
ConvertToV8(isolate, obj.get()).ToLocalChecked(),
};
func->Call(context_.Get(isolate), v8::Undefined(isolate), 1, argv)
.ToLocalChecked();
EXPECT_FALSE(try_catch.HasCaught());
EXPECT_EQ("", try_catch.GetStackTrace());
EXPECT_EQ(191, obj->value());
}
TEST_F(WrappableTest, MethodInvocationErrorsOnUnnamedObject) {
v8::Isolate* isolate = instance_->isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = context_.Get(isolate);
gin::Handle<MyObject> obj = MyObject::Create(isolate);
v8::Local<v8::Object> v8_object =
ConvertToV8(isolate, obj.get()).ToLocalChecked().As<v8::Object>();
v8::Local<v8::Value> member_method =
v8_object->Get(context, StringToV8(isolate, "memberMethod"))
.ToLocalChecked();
ASSERT_TRUE(member_method->IsFunction());
v8::Local<v8::Value> non_member_method =
v8_object->Get(context, StringToV8(isolate, "nonMemberMethod"))
.ToLocalChecked();
ASSERT_TRUE(non_member_method->IsFunction());
auto get_error = [isolate, context](v8::Local<v8::Value> function_to_run,
v8::Local<v8::Value> context_object) {
constexpr char kScript[] =
"(function(toRun, contextObject) { toRun.apply(contextObject, []); })";
v8::Local<v8::String> source = StringToV8(isolate, kScript);
EXPECT_FALSE(source.IsEmpty());
v8::TryCatch try_catch(isolate);
v8::Local<v8::Script> script =
v8::Script::Compile(context, source).ToLocalChecked();
v8::Local<v8::Value> val = script->Run(context).ToLocalChecked();
v8::Local<v8::Function> func;
EXPECT_TRUE(ConvertFromV8(isolate, val, &func));
v8::Local<v8::Value> argv[] = {function_to_run, context_object};
func->Call(context, v8::Undefined(isolate), std::size(argv), argv)
.FromMaybe(v8::Local<v8::Value>());
if (!try_catch.HasCaught())
return std::string();
return V8ToString(isolate, try_catch.Message()->Get());
};
EXPECT_EQ(std::string(), get_error(member_method, v8_object));
EXPECT_EQ(std::string(), get_error(non_member_method, v8_object));
EXPECT_EQ("Uncaught TypeError: Illegal invocation",
get_error(member_method, v8::Null(isolate)));
// A non-member function shouldn't throw errors for being applied on a
// null (or invalid) object.
EXPECT_EQ(std::string(), get_error(non_member_method, v8::Null(isolate)));
v8::Local<v8::Object> wrong_object = v8::Object::New(isolate);
// We should get an error for passing the wrong object.
EXPECT_EQ("Uncaught TypeError: Illegal invocation",
get_error(member_method, wrong_object));
// But again, not for a "static" method.
EXPECT_EQ(std::string(), get_error(non_member_method, v8::Null(isolate)));
}
TEST_F(WrappableTest, MethodInvocationErrorsOnNamedObject) {
v8::Isolate* isolate = instance_->isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = context_.Get(isolate);
gin::Handle<MyNamedObject> obj = MyNamedObject::Create(isolate);
v8::Local<v8::Object> v8_object =
ConvertToV8(isolate, obj.get()).ToLocalChecked().As<v8::Object>();
v8::Local<v8::Value> member_method =
v8_object->Get(context, StringToV8(isolate, "memberMethod"))
.ToLocalChecked();
ASSERT_TRUE(member_method->IsFunction());
v8::Local<v8::Value> non_member_method =
v8_object->Get(context, StringToV8(isolate, "nonMemberMethod"))
.ToLocalChecked();
ASSERT_TRUE(non_member_method->IsFunction());
auto get_error = [isolate, context](v8::Local<v8::Value> function_to_run,
v8::Local<v8::Value> context_object) {
constexpr char kScript[] =
"(function(toRun, contextObject) { toRun.apply(contextObject, []); })";
v8::Local<v8::String> source = StringToV8(isolate, kScript);
EXPECT_FALSE(source.IsEmpty());
v8::TryCatch try_catch(isolate);
v8::Local<v8::Script> script =
v8::Script::Compile(context, source).ToLocalChecked();
v8::Local<v8::Value> val = script->Run(context).ToLocalChecked();
v8::Local<v8::Function> func;
EXPECT_TRUE(ConvertFromV8(isolate, val, &func));
v8::Local<v8::Value> argv[] = {function_to_run, context_object};
func->Call(context, v8::Undefined(isolate), std::size(argv), argv)
.FromMaybe(v8::Local<v8::Value>());
if (!try_catch.HasCaught())
return std::string();
return V8ToString(isolate, try_catch.Message()->Get());
};
EXPECT_EQ(std::string(), get_error(member_method, v8_object));
EXPECT_EQ(std::string(), get_error(non_member_method, v8_object));
EXPECT_EQ(
"Uncaught TypeError: Illegal invocation: Function must be called on "
"an object of type MyNamedObject",
get_error(member_method, v8::Null(isolate)));
// A non-member function shouldn't throw errors for being applied on a
// null (or invalid) object.
EXPECT_EQ(std::string(), get_error(non_member_method, v8::Null(isolate)));
v8::Local<v8::Object> wrong_object = v8::Object::New(isolate);
// We should get an error for passing the wrong object.
EXPECT_EQ(
"Uncaught TypeError: Illegal invocation: Function must be called on "
"an object of type MyNamedObject",
get_error(member_method, wrong_object));
// But again, not for a "static" method.
EXPECT_EQ(std::string(), get_error(non_member_method, v8::Null(isolate)));
}
class MyObjectWithLazyProperties
: public Wrappable<MyObjectWithLazyProperties> {
public:
MyObjectWithLazyProperties(const MyObjectWithLazyProperties&) = delete;
MyObjectWithLazyProperties& operator=(const MyObjectWithLazyProperties&) =
delete;
static WrapperInfo kWrapperInfo;
static gin::Handle<MyObjectWithLazyProperties> Create(v8::Isolate* isolate) {
return CreateHandle(isolate, new MyObjectWithLazyProperties());
}
int access_count() const { return access_count_; }
private:
MyObjectWithLazyProperties() = default;
ObjectTemplateBuilder GetObjectTemplateBuilder(v8::Isolate* isolate) final {
return Wrappable::GetObjectTemplateBuilder(isolate)
.SetLazyDataProperty("fortyTwo", &MyObjectWithLazyProperties::FortyTwo)
.SetLazyDataProperty("self",
base::BindRepeating([](gin::Arguments* arguments) {
v8::Local<v8::Value> holder;
CHECK(arguments->GetHolder(&holder));
return holder;
}));
}
int FortyTwo() {
access_count_++;
return 42;
}
int access_count_ = 0;
};
WrapperInfo MyObjectWithLazyProperties::kWrapperInfo = {kEmbedderNativeGin};
TEST_F(WrappableTest, LazyPropertyGetterIsCalledOnce) {
v8::Isolate* isolate = instance_->isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = context_.Get(isolate);
auto handle = MyObjectWithLazyProperties::Create(isolate);
v8::Local<v8::Object> v8_object = handle.ToV8().As<v8::Object>();
v8::Local<v8::String> key = StringToSymbol(isolate, "fortyTwo");
v8::Local<v8::Value> value;
bool has_own_property = false;
ASSERT_TRUE(v8_object->HasOwnProperty(context, key).To(&has_own_property));
EXPECT_TRUE(has_own_property);
EXPECT_EQ(0, handle->access_count());
ASSERT_TRUE(v8_object->Get(context, key).ToLocal(&value));
EXPECT_TRUE(value->StrictEquals(v8::Int32::New(isolate, 42)));
EXPECT_EQ(1, handle->access_count());
ASSERT_TRUE(v8_object->Get(context, key).ToLocal(&value));
EXPECT_TRUE(value->StrictEquals(v8::Int32::New(isolate, 42)));
EXPECT_EQ(1, handle->access_count());
}
TEST_F(WrappableTest, LazyPropertyGetterCanBeSetFirst) {
v8::Isolate* isolate = instance_->isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = context_.Get(isolate);
auto handle = MyObjectWithLazyProperties::Create(isolate);
v8::Local<v8::Object> v8_object = handle.ToV8().As<v8::Object>();
v8::Local<v8::String> key = StringToSymbol(isolate, "fortyTwo");
v8::Local<v8::Value> value;
EXPECT_EQ(0, handle->access_count());
bool set_ok = false;
ASSERT_TRUE(
v8_object->Set(context, key, v8::Int32::New(isolate, 1701)).To(&set_ok));
ASSERT_TRUE(set_ok);
ASSERT_TRUE(v8_object->Get(context, key).ToLocal(&value));
EXPECT_TRUE(value->StrictEquals(v8::Int32::New(isolate, 1701)));
EXPECT_EQ(0, handle->access_count());
}
TEST_F(WrappableTest, LazyPropertyGetterCanBindSpecialArguments) {
v8::Isolate* isolate = instance_->isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = context_.Get(isolate);
auto handle = MyObjectWithLazyProperties::Create(isolate);
v8::Local<v8::Object> v8_object = handle.ToV8().As<v8::Object>();
v8::Local<v8::Value> value;
ASSERT_TRUE(
v8_object->Get(context, StringToSymbol(isolate, "self")).ToLocal(&value));
EXPECT_TRUE(v8_object == value);
}
TEST_F(WrappableTest, CannotConstruct) {
v8::Isolate* isolate = instance_->isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = context_.Get(isolate);
Handle<MyObject> obj = MyObject::Create(isolate);
v8::Local<v8::Value> wrapper =
ConvertToV8(isolate, obj.get()).ToLocalChecked();
ASSERT_FALSE(wrapper.IsEmpty());
v8::Local<v8::String> source =
StringToV8(isolate, "(obj => new obj.constructor())");
v8::Local<v8::Script> script =
v8::Script::Compile(context, source).ToLocalChecked();
v8::Local<v8::Function> function =
script->Run(context).ToLocalChecked().As<v8::Function>();
{
TryCatch try_catch(isolate);
EXPECT_TRUE(function
->Call(context, v8::Undefined(isolate), 1,
(v8::Local<v8::Value>[]){wrapper})
.IsEmpty());
EXPECT_TRUE(try_catch.HasCaught());
}
}
} // namespace gin