| // 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/cppgc/allocation.h" |
| #include"v8/include/v8-cppgc.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. |
| voidNonMemberMethod(){} |
| |
| // 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. |
| classBaseClass{ |
| public: |
| BaseClass()=default; |
| BaseClass(constBaseClass&)=delete; |
| BaseClass&operator=(constBaseClass&)=delete; |
| virtual~BaseClass()=default; |
| |
| // So the compiler doesn't complain that |value_| is unused. |
| int value()const{return value_;} |
| |
| private: |
| int value_=23; |
| }; |
| |
| classMyObject:publicWrappable<MyObject>,publicBaseClass{ |
| public: |
| MyObject(constMyObject&)=delete; |
| MyObject&operator=(constMyObject&)=delete; |
| |
| MyObject()=default; |
| |
| staticMyObject*Create(v8::Isolate* isolate){ |
| return cppgc::MakeGarbageCollected<MyObject>( |
| isolate->GetCppHeap()->GetAllocationHandle()); |
| } |
| |
| int value()const{return value_;} |
| void set_value(int value){ value_= value;} |
| |
| voidMethod(){} |
| |
| staticconstexprWrapperInfo kWrapperInfo={{kEmbedderNativeGin}, |
| kTestObject}; |
| |
| constWrapperInfo* wrapper_info()const override{return&kWrapperInfo;} |
| |
| constchar*GetHumanReadableName()const final{return"MyObject";} |
| |
| protected: |
| ObjectTemplateBuilderGetObjectTemplateBuilder(v8::Isolate* isolate) final{ |
| returnWrappable<MyObject>::GetObjectTemplateBuilder(isolate) |
| .SetProperty("value",&MyObject::value,&MyObject::set_value) |
| .SetMethod("memberMethod",&MyObject::Method) |
| .SetMethod("nonMemberMethod",&NonMemberMethod); |
| } |
| |
| private: |
| int value_=0; |
| }; |
| |
| classMyObject2:publicWrappable<MyObject2>{ |
| public: |
| MyObject2()=default; |
| |
| staticconstexprWrapperInfo kWrapperInfo={{kEmbedderNativeGin}, |
| kTestObject2}; |
| |
| constWrapperInfo* wrapper_info()const override{return&kWrapperInfo;} |
| |
| constchar*GetHumanReadableName()const final{return"MyObject2";} |
| |
| staticMyObject2*Create(v8::Isolate* isolate){ |
| return cppgc::MakeGarbageCollected<MyObject2>( |
| isolate->GetCppHeap()->GetAllocationHandle()); |
| } |
| }; |
| |
| classMyNamedObject:publicWrappable<MyNamedObject>{ |
| public: |
| MyNamedObject(constMyNamedObject&)=delete; |
| MyNamedObject&operator=(constMyNamedObject&)=delete; |
| MyNamedObject()=default; |
| |
| staticconstexprWrapperInfo kWrapperInfo={{kEmbedderNativeGin}, |
| kTestObject2}; |
| |
| constWrapperInfo* wrapper_info()const override{return&kWrapperInfo;} |
| |
| staticMyNamedObject*Create(v8::Isolate* isolate){ |
| return cppgc::MakeGarbageCollected<MyNamedObject>( |
| isolate->GetCppHeap()->GetAllocationHandle()); |
| } |
| |
| voidMethod(){} |
| |
| protected: |
| ObjectTemplateBuilderGetObjectTemplateBuilder(v8::Isolate* isolate) final{ |
| returnWrappable<MyNamedObject>::GetObjectTemplateBuilder(isolate) |
| .SetMethod("memberMethod",&MyNamedObject::Method) |
| .SetMethod("nonMemberMethod",&NonMemberMethod); |
| } |
| constchar*GetHumanReadableName()const final{return"MyNamedObject";} |
| }; |
| |
| }// namespace |
| |
| typedef V8TestWrappableTest; |
| |
| TEST_F(WrappableTest,WrapAndUnwrap){ |
| v8::Isolate* isolate= instance_->isolate(); |
| v8::HandleScope handle_scope(isolate); |
| |
| MyObject* obj=MyObject::Create(isolate); |
| |
| v8::Local<v8::Value> wrapper=ConvertToV8(isolate, obj).ToLocalChecked(); |
| EXPECT_FALSE(wrapper.IsEmpty()); |
| |
| MyObject* unwrapped=nullptr; |
| EXPECT_TRUE(ConvertFromV8(isolate, wrapper,&unwrapped)); |
| EXPECT_EQ(obj, unwrapped); |
| } |
| |
| TEST_F(WrappableTest,UnwrapNull){ |
| v8::Isolate* isolate= instance_->isolate(); |
| v8::HandleScope handle_scope(isolate); |
| |
| MyObject* obj=nullptr; |
| v8::Local<v8::Value> wrapper=ConvertToV8(isolate, obj).ToLocalChecked(); |
| EXPECT_FALSE(wrapper.IsEmpty()); |
| |
| MyObject* unwrapped=nullptr; |
| ConvertFromV8(isolate, wrapper,&unwrapped); |
| EXPECT_EQ(obj, unwrapped); |
| } |
| |
| TEST_F(WrappableTest,GetAndSetProperty){ |
| v8::Isolate* isolate= instance_->isolate(); |
| v8::HandleScope handle_scope(isolate); |
| |
| 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).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); |
| |
| MyObject* obj=MyObject::Create(isolate); |
| |
| v8::Local<v8::Object> v8_object= |
| ConvertToV8(isolate, obj).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){ |
| constexprchar 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_TRUE(get_error(member_method, v8::Null(isolate)) |
| .starts_with("Uncaught TypeError: Illegal invocation")); |
| // 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_TRUE(get_error(member_method, wrong_object) |
| .starts_with("Uncaught TypeError: Illegal invocation")); |
| // 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); |
| |
| MyNamedObject* obj=MyNamedObject::Create(isolate); |
| |
| v8::Local<v8::Object> v8_object= |
| ConvertToV8(isolate, obj).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){ |
| constexprchar 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))); |
| } |
| |
| classMyObjectWithLazyProperties |
| :publicWrappable<MyObjectWithLazyProperties>{ |
| public: |
| MyObjectWithLazyProperties(constMyObjectWithLazyProperties&)=delete; |
| MyObjectWithLazyProperties&operator=(constMyObjectWithLazyProperties&)= |
| delete; |
| MyObjectWithLazyProperties()=default; |
| |
| staticconstexprWrapperInfo kWrapperInfo={{kEmbedderNativeGin}, |
| kTestObject}; |
| |
| constWrapperInfo* wrapper_info()const override{return&kWrapperInfo;} |
| |
| constchar*GetHumanReadableName()const final{ |
| return"MyObjectWithLazyProperties"; |
| } |
| |
| staticMyObjectWithLazyProperties*Create(v8::Isolate* isolate){ |
| return cppgc::MakeGarbageCollected<MyObjectWithLazyProperties>( |
| isolate->GetCppHeap()->GetAllocationHandle()); |
| } |
| |
| int access_count()const{return access_count_;} |
| |
| private: |
| ObjectTemplateBuilderGetObjectTemplateBuilder(v8::Isolate* isolate) final{ |
| returnWrappable::GetObjectTemplateBuilder(isolate) |
| .SetLazyDataProperty("fortyTwo",&MyObjectWithLazyProperties::FortyTwo) |
| .SetLazyDataProperty("self", |
| base::BindRepeating([](gin::Arguments* arguments){ |
| v8::Local<v8::Value> holder; |
| CHECK(arguments->GetHolder(&holder)); |
| return holder; |
| })); |
| } |
| |
| intFortyTwo(){ |
| access_count_++; |
| return42; |
| } |
| |
| int access_count_=0; |
| }; |
| |
| TEST_F(WrappableTest,LazyPropertyGetterIsCalledOnce){ |
| v8::Isolate* isolate= instance_->isolate(); |
| v8::HandleScope handle_scope(isolate); |
| v8::Local<v8::Context> context= context_.Get(isolate); |
| |
| MyObjectWithLazyProperties* obj=MyObjectWithLazyProperties::Create(isolate); |
| v8::Local<v8::Object> v8_object= |
| ConvertToV8(isolate, obj).ToLocalChecked().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, obj->access_count()); |
| |
| ASSERT_TRUE(v8_object->Get(context, key).ToLocal(&value)); |
| EXPECT_TRUE(value->StrictEquals(v8::Int32::New(isolate,42))); |
| EXPECT_EQ(1, obj->access_count()); |
| |
| ASSERT_TRUE(v8_object->Get(context, key).ToLocal(&value)); |
| EXPECT_TRUE(value->StrictEquals(v8::Int32::New(isolate,42))); |
| EXPECT_EQ(1, obj->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* obj=MyObjectWithLazyProperties::Create(isolate); |
| v8::Local<v8::Object> v8_object= |
| ConvertToV8(isolate, obj).ToLocalChecked().As<v8::Object>(); |
| v8::Local<v8::String> key=StringToSymbol(isolate,"fortyTwo"); |
| v8::Local<v8::Value> value; |
| |
| EXPECT_EQ(0, obj->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, obj->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* obj=MyObjectWithLazyProperties::Create(isolate); |
| v8::Local<v8::Object> v8_object= |
| ConvertToV8(isolate, obj).ToLocalChecked().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); |
| |
| MyObject* obj=MyObject::Create(isolate); |
| v8::Local<v8::Value> wrapper=ConvertToV8(isolate, obj).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 |