diff --git a/internal/testing/htmltest/script_context_helper.go b/internal/testing/htmltest/script_context_helper.go new file mode 100644 index 00000000..1f4c7fc6 --- /dev/null +++ b/internal/testing/htmltest/script_context_helper.go @@ -0,0 +1,25 @@ +package htmltest + +import ( + "testing" + + "github.com/gost-dom/browser/html" +) + +type ScriptContextHelper struct { + html.ScriptContext + t testing.TB +} + +func NewScriptContextHelper(t testing.TB, ctx html.ScriptContext) ScriptContextHelper { + return ScriptContextHelper{ctx, t} +} + +func (h ScriptContextHelper) MustEval(script string) any { + h.t.Helper() + res, err := h.Eval(script) + if err != nil { + h.t.Fatalf("Script error: %v", err) + } + return res +} diff --git a/scripting/internal/js/arguments.go b/scripting/internal/js/arguments.go index 95198790..a7a11369 100644 --- a/scripting/internal/js/arguments.go +++ b/scripting/internal/js/arguments.go @@ -48,8 +48,6 @@ func ConsumeArgument[T, U any]( } } -func IsUndefined[T any](v Value[T]) bool { return v == nil || v.IsUndefined() } - func ConsumeRestArguments[T, U any]( args CallbackContext[U], name string, diff --git a/scripting/internal/js/array.go b/scripting/internal/js/array.go new file mode 100644 index 00000000..a855f983 --- /dev/null +++ b/scripting/internal/js/array.go @@ -0,0 +1,6 @@ +package js + +type Array[T any] interface { + Value[T] + Push(Value[T]) error +} diff --git a/scripting/internal/js/callback_context.go b/scripting/internal/js/callback_context.go index baf140fb..a15ca9f9 100644 --- a/scripting/internal/js/callback_context.go +++ b/scripting/internal/js/callback_context.go @@ -169,6 +169,7 @@ type ValueFactory[T any] interface { NewPromise() Promise[T] NewString(string) Value[T] + NewNumber(float64) Value[T] NewBoolean(bool) Value[T] NewObject() Object[T] NewFunction(Callback[T]) Function[T] @@ -179,7 +180,7 @@ type ValueFactory[T any] interface { // NewArray creates a JavaScript array containing the values. If any value // is nil, it will become undefined in the resulting array. - NewArray(...Value[T]) Value[T] + NewArray(...Value[T]) Array[T] // NewIterator returns an object implementing the [Iterator protocol] // // [Iterator protocol]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol diff --git a/scripting/internal/js/value.go b/scripting/internal/js/value.go index a5d54a6a..7790d2e3 100644 --- a/scripting/internal/js/value.go +++ b/scripting/internal/js/value.go @@ -1,5 +1,12 @@ package js +import ( + "errors" + "fmt" + + "github.com/gost-dom/browser/internal/constants" +) + // Value represents a value in JavaScript. Referential equality cannot be used // to check if to Value instances represent the same value in JavaScript. Use // StrictEquals to check if two values are equal. @@ -15,6 +22,7 @@ type Value[T any] interface { Self() T String() string + Number() float64 Int32() int32 Uint32() uint32 Boolean() bool @@ -23,9 +31,11 @@ type Value[T any] interface { IsNull() bool IsSymbol() bool IsString() bool + IsNumber() bool IsObject() bool IsBoolean() bool IsFunction() bool + IsArray() bool AsFunction() (Function[T], bool) AsObject() (Object[T], bool) @@ -33,6 +43,12 @@ type Value[T any] interface { StrictEquals(Value[T]) bool } +// IsNullish returns whether a JavaScript value is null or undefined. +func IsNullish[T any](v Value[T]) bool { return v == nil || v.IsNull() || v.IsUndefined() } + +func IsUndefined[T any](v Value[T]) bool { return v == nil || v.IsUndefined() } +func IsBoolean[T any](v Value[T]) bool { return v != nil && v.IsBoolean() } + type Function[T any] interface { Value[T] @@ -100,14 +116,108 @@ type Promise[T any] interface { Reject(Value[T]) } -// IsNullish returns whether a JavaScript value is null or undefined. -func IsNullish[T any](v Value[T]) bool { return v == nil || v.IsNull() || v.IsUndefined() } - -func IsBoolean[T any](v Value[T]) bool { return v != nil && v.IsBoolean() } - func AsFunction[T any](v Value[T]) (Function[T], bool) { if IsNullish(v) { return nil, false } return v.AsFunction() } + +func Clone[T any](v Value[T], s Scope[T]) (Value[T], error) { + var objects [][2]Value[T] + return clone(v, s, &objects) +} + +func clone[T any](v Value[T], s Scope[T], objects *[][2]Value[T]) (Value[T], error) { + switch { + case v == nil || v.IsUndefined(): + return s.Undefined(), nil + case v.IsNull(): + return s.Null(), nil + case v.IsUndefined(): + return s.Undefined(), nil + case v.IsString(): + return s.NewString(v.String()), nil + case v.IsNumber(): + return s.NewNumber(v.Number()), nil + case v.IsBoolean(): + return s.NewBoolean(v.Boolean()), nil + case v.IsArray(): + return cloneArray(v, s, objects) + case v.IsFunction(): + //TODO: Use correct error + return nil, errors.New("Serialize function") + } + if o, ok := v.AsObject(); ok { + return cloneObject(o, s, objects) + } + return nil, fmt.Errorf("Unable to clone value: %v", v) +} + +func findKnownValue[T any](o Value[T], knownObjects *[][2]Value[T]) (Value[T], bool) { + for _, pair := range *knownObjects { + known := pair[0] + res := pair[1] + if o.StrictEquals(known) { + return res, true + } + } + return nil, false +} + +func cloneArray[T any]( + v Value[T], + s Scope[T], + knownObjects *[][2]Value[T], +) (Value[T], error) { + if existing, ok := findKnownValue(v, knownObjects); ok { + return existing, nil + } + o, ok := v.AsObject() + if !ok { + return nil, fmt.Errorf( + "Object was an array, but not convertible to object. %w", + constants.ErrGostDomBug, + ) + } + res := s.NewArray() + *knownObjects = append(*knownObjects, [2]Value[T]{v, res}) + + for v, err := range Iterate(o) { + if err != nil { + return nil, err + } + cloned, err := clone(v, s, knownObjects) + if err != nil { + return nil, err + } + res.Push(cloned) + } + // TODO: Potential bug here, if the array references itself recursively, + // this would lead to a stack overflow error. + return res, nil +} + +func cloneObject[T any](o Object[T], s Scope[T], knownObjects *[][2]Value[T]) (Value[T], error) { + if existing, ok := findKnownValue(o, knownObjects); ok { + return existing, nil + } + res := s.NewObject() + *knownObjects = append(*knownObjects, [2]Value[T]{o, res}) + keys, err := o.Keys() + if err != nil { + return nil, err + } + for _, k := range keys { + oldV, err := o.Get(k) + if err != nil { + return nil, err + } + newV, err := clone(oldV, s, knownObjects) + if err != nil { + return nil, err + } + res.Set(k, newV) + } + return res, nil +} diff --git a/scripting/internal/scripttests/engine_suites.go b/scripting/internal/scripttests/engine_suites.go new file mode 100644 index 00000000..c3d5f688 --- /dev/null +++ b/scripting/internal/scripttests/engine_suites.go @@ -0,0 +1,96 @@ +package scripttests + +import ( + "fmt" + "testing" + + "github.com/gost-dom/browser/html" + "github.com/gost-dom/browser/internal/entity" + "github.com/gost-dom/browser/internal/testing/htmltest" + "github.com/gost-dom/browser/scripting/internal/js" + "github.com/stretchr/testify/assert" +) + +// RunScriptEngineSuites runs test suites of the script engine without a +// predefined global scope. In contrast to [RunSuites] that expect an engine +// configured for the global scope in the Window realm. +func RunScriptEngineSuites[T any](t *testing.T, f ScriptEngineFactory[T]) { + type Global = entity.Entity + + e := f(js.ConfigurerFunc[T](func(e js.ScriptEngine[T]) { + global := e.ConfigureGlobalScope("Global", nil) + global.CreateOperation("store", func(ctx js.CallbackContext[T]) (js.Value[T], error) { + v, ok := ctx.ConsumeArg() + if !ok { + return nil, ctx.NewTypeError("Missing argument") + } + c, err := js.As[entity.Components](ctx.GlobalThis().NativeValue(), nil) + if err != nil { + return nil, err + } + entity.SetComponentType(c, v) + return nil, nil + }) + + global.CreateOperation("get", func(ctx js.CallbackContext[T]) (js.Value[T], error) { + c, err := js.As[entity.Components](ctx.GlobalThis().NativeValue(), nil) + if err != nil { + return nil, err + } + val, ok := entity.ComponentType[js.Value[T]](c) + if !ok { + return nil, fmt.Errorf("Value missing") + } + + res, err := js.Clone(val, ctx) + if err != nil { + t.Errorf("Clone error: %v", err) + } + return res, err + }) + })) + + global1 := new(Global) + global2 := new(Global) + + c1 := htmltest.NewScriptContextHelper( + t, + e.NewHost(html.ScriptEngineOptions{}).NewContext(dummyContext{global1, t.Context()}), + ) + c2 := htmltest.NewScriptContextHelper( + t, + e.NewHost(html.ScriptEngineOptions{}).NewContext(dummyContext{global2, t.Context()}), + ) + + assert.NoError(t, c1.Run(` + const b = { + id: "b", + } + const arr = [1,2,3] + const recursiveArray = [1,2,3] + recursiveArray.push(recursiveArray) + const a = { + stringVal: "hello", + numberVal: 42.5, + trueVal: true, + falseVal: false, + b1: b, + b2: b, + arr1: arr, + arr2: arr, + recursiveArray, + } + globalThis.store(a) + `)) + val, ok := entity.ComponentType[js.Value[T]](global1) + assert.True(t, ok) + entity.SetComponentType(global2, val) + + assert.NoError(t, c2.Run("const cloned = globalThis.get()")) + assert.Equal(t, "hello", c2.MustEval("cloned.stringVal")) + assert.Equal(t, 42.5, c2.MustEval("cloned.numberVal")) + assert.True(t, c2.MustEval("cloned.trueVal").(bool)) + assert.False(t, c2.MustEval("cloned.falseVal").(bool)) + assert.True(t, c2.MustEval("cloned.b1 === cloned.b2").(bool)) + assert.Equal(t, "1,2,3", c2.MustEval("cloned.arr1.join(',')")) +} diff --git a/scripting/internal/scripttests/suites.go b/scripting/internal/scripttests/suites.go index ea3e5c99..1667518c 100644 --- a/scripting/internal/scripttests/suites.go +++ b/scripting/internal/scripttests/suites.go @@ -1,12 +1,17 @@ package scripttests import ( + "context" + "log/slog" + "net/http" "testing" "github.com/gost-dom/browser/html" + "github.com/gost-dom/browser/internal/entity" "github.com/gost-dom/browser/internal/testing/browsertest" "github.com/gost-dom/browser/scripting/internal/dom/domsuite" "github.com/gost-dom/browser/scripting/internal/html/htmlsuite" + "github.com/gost-dom/browser/scripting/internal/js" "github.com/gost-dom/browser/scripting/internal/uievents/uieventssuite" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -28,9 +33,11 @@ func RunBasicSuite(t *testing.T, e html.ScriptEngine) { assert.True(t, w.MustEval("window === globalThis").(bool)) } +type ScriptEngineFactory[T any] = func(js.Configurer[T]) html.ScriptEngine + func RunSuites(t *testing.T, e html.ScriptEngine) { t.Run("ScriptEngineBehaviour", func(t *testing.T) { testScriptEngineBehaviour(t, e) }) - t.Run("SharowRoot", runSuite(NewShadowRootSuite(e))) + t.Run("ShadowRoot", runSuite(NewShadowRootSuite(e))) t.Run("DocumentFragment", runSuite(NewDocumentFragmentSuite(e))) t.Run("XMLHttpRequest", runSuite(NewXMLHttpRequestSuite(e))) t.Run("Location", runSuite(NewLocationSuite(e))) @@ -58,3 +65,13 @@ func RunSuites(t *testing.T, e html.ScriptEngine) { t.Run("html", func(t *testing.T) { htmlsuite.RunHtmlSuite(t, e) }) t.Run("dom", func(t *testing.T) { domsuite.RunDomSuite(t, e) }) } + +type dummyContext struct { + *entity.Entity + ctx context.Context +} + +func (c dummyContext) Context() context.Context { return c.ctx } +func (c dummyContext) HTTPClient() http.Client { return *http.DefaultClient } +func (c dummyContext) LocationHREF() string { return "http://example.com" } +func (c dummyContext) Logger() *slog.Logger { return nil } diff --git a/scripting/sobekengine/array.go b/scripting/sobekengine/array.go new file mode 100644 index 00000000..4a249547 --- /dev/null +++ b/scripting/sobekengine/array.go @@ -0,0 +1,31 @@ +package sobekengine + +import ( + "errors" + + "github.com/grafana/sobek" +) + +type array struct { + value + obj *sobek.Object +} + +func newArray(c *scriptContext, o *sobek.Object) jsArray { + return array{value{c, o}, o} +} + +func (a array) Push(v jsValue) error { + push := a.obj.Get("push") + if push == nil { + return errors.New("gost-dom/sobekengine: array.push: Underlying object doesn't have a push") + } + p, ok := sobek.AssertFunction(push) + if !ok { + return errors.New( + "gost-dom/sobekengine: array.push: Underlying object's push is not a function", + ) + } + _, err := p(a.obj, v.Self().value) + return err +} diff --git a/scripting/sobekengine/scope.go b/scripting/sobekengine/scope.go index e19c04d0..0f750ef9 100644 --- a/scripting/sobekengine/scope.go +++ b/scripting/sobekengine/scope.go @@ -64,12 +64,12 @@ func (f scope) JSONStringify(v js.Value[jsTypeParam]) string { panic(fmt.Sprintf("gost-dom/sobekhost: JSONStringify only supports objects. Got: %v", v)) } -func (f scope) NewArray(v ...js.Value[jsTypeParam]) js.Value[jsTypeParam] { +func (f scope) NewArray(v ...js.Value[jsTypeParam]) jsArray { arr := make([]any, len(v)) for i, val := range v { arr[i] = unwrapValue(val) } - return newObject(f.scriptContext, f.vm.NewArray(arr...)) + return newArray(f.scriptContext, f.vm.NewArray(arr...)) } func (f scope) NewBoolean(v bool) js.Value[jsTypeParam] { @@ -100,6 +100,10 @@ func (f scope) NewString(v string) js.Value[jsTypeParam] { return newValue(f.scriptContext, f.vm.ToValue(v)) } +func (f scope) NewNumber(v float64) js.Value[jsTypeParam] { + return newValue(f.scriptContext, f.vm.ToValue(v)) +} + // NewTypeError implements [js.ValueFactory]. func (c scope) NewTypeError(v string) js.Error[jsTypeParam] { sobekErrVal := c.vm.NewTypeError(v) diff --git a/scripting/sobekengine/script_context.go b/scripting/sobekengine/script_context.go index 87a80bd6..1f5038a9 100644 --- a/scripting/sobekengine/script_context.go +++ b/scripting/sobekengine/script_context.go @@ -371,7 +371,17 @@ func (c *scriptContext) typeOf(v value) string { panic(err) } return res.String() +} +func (c *scriptContext) isArray(v value) bool { + vm := c.vm + fn, _ := vm.RunString("x => Array.isArray(x)") + fnn, _ := sobek.AssertFunction(fn) + res, err := fnn(vm.GlobalObject(), v.value) + if err != nil { + panic(err) + } + return res.ToBoolean() } /* -------- script -------- */ diff --git a/scripting/sobekengine/sobek_test.go b/scripting/sobekengine/sobek_test.go index 8e19f6ae..b4e414dd 100644 --- a/scripting/sobekengine/sobek_test.go +++ b/scripting/sobekengine/sobek_test.go @@ -3,7 +3,9 @@ package sobekengine import ( "testing" + "github.com/gost-dom/browser/html" "github.com/gost-dom/browser/scripting/internal" + "github.com/gost-dom/browser/scripting/internal/js" "github.com/gost-dom/browser/scripting/internal/scripttests" "github.com/gost-dom/browser/scripting/internal/testing/jsassert" ) @@ -35,6 +37,12 @@ func TestBasics(t *testing.T) { scripttests.RunBasicSuite(t, assertEngine) } +func TestSobekEngine(t *testing.T) { + scripttests.RunScriptEngineSuites(t, + func(c js.Configurer[jsTypeParam]) html.ScriptEngine { return newEngine(c) }, + ) +} + func init() { configurer := internal.CreateWindowsConfigurer[jsTypeParam]() configurer.AddConfigurerFunc(jsassert.Configure) diff --git a/scripting/sobekengine/value.go b/scripting/sobekengine/value.go index 41752b2f..97086a68 100644 --- a/scripting/sobekengine/value.go +++ b/scripting/sobekengine/value.go @@ -8,6 +8,7 @@ import ( type jsTypeParam = value type jsValue = js.Value[jsTypeParam] type jsObject = js.Object[jsTypeParam] +type jsArray = js.Array[jsTypeParam] type jsFunction = js.Function[jsTypeParam] type jsError = js.Error[jsTypeParam] @@ -40,16 +41,19 @@ func (v value) AsFunction() (js.Function[jsTypeParam], bool) { } func (v value) AsObject() (jsObject, bool) { - if o := v.value.ToObject(v.ctx.vm); o != nil { - return newObject(v.ctx, o), true + if !v.IsObject() { + return nil, false } - return nil, false + o := v.value.ToObject(v.ctx.vm) + return newObject(v.ctx, o), true } func (v value) IsNull() bool { return sobek.IsNull(v.value) } func (v value) IsUndefined() bool { return sobek.IsUndefined(v.value) } func (v value) IsString() bool { return sobek.IsString(v.value) } +func (v value) IsNumber() bool { return sobek.IsNumber(v.value) } +func (v value) IsArray() bool { return v.ctx.isArray(v) } func (v value) IsBoolean() bool { // Sobek doesn't expose an IsBoolean function, so resort to calling 'typeof' @@ -80,6 +84,9 @@ func (v value) IsFunction() bool { } func (v value) String() string { return v.value.String() } + +func (v value) Number() float64 { return v.value.ToFloat() } + func (v value) Boolean() bool { return v.value.ToBoolean() } func (v value) Int32() int32 { return int32(v.value.ToInteger()) } func (v value) Uint32() uint32 { return uint32(v.value.ToInteger()) } diff --git a/scripting/v8engine/array.go b/scripting/v8engine/array.go new file mode 100644 index 00000000..18ae7697 --- /dev/null +++ b/scripting/v8engine/array.go @@ -0,0 +1,25 @@ +package v8engine + +import "github.com/gost-dom/v8go" + +type v8Array struct { + v8Value + Object *v8go.Object +} + +func newV8Array(ctx *V8ScriptContext, o *v8go.Object) jsArray { + return &v8Array{v8Value{ctx, o.Value}, o} +} + +func (a *v8Array) Push(v jsValue) error { + push, err := a.Object.Get("push") + if err != nil { + return err + } + f, err := push.AsFunction() + if err != nil { + return err + } + _, err = f.Call(toV8Value(&a.v8Value), toV8Value(v)) + return err +} diff --git a/scripting/v8engine/callback_context.go b/scripting/v8engine/callback_context.go index b3307b72..028e0126 100644 --- a/scripting/v8engine/callback_context.go +++ b/scripting/v8engine/callback_context.go @@ -116,7 +116,8 @@ func (f v8Scope) iso() *v8go.Isolate { return f.host.iso } func (f v8Scope) Undefined() jsValue { return f.toJSValue(v8go.Undefined(f.iso())) } func (f v8Scope) Null() jsValue { return f.toJSValue(v8go.Null(f.iso())) } -func (f v8Scope) NewString(val string) jsValue { return f.newV8Value(val) } +func (f v8Scope) NewString(val string) jsValue { return f.newV8Value(val) } +func (f v8Scope) NewNumber(val float64) jsValue { return f.newV8Value(val) } func (f v8Scope) NewObject() jsObject { val, err := f.V8ScriptContext.v8ctx.RunScript("({})", "gost-dom/object") @@ -196,7 +197,7 @@ func (f v8Scope) JSONParse(val string) (jsValue, error) { } -func (f v8Scope) NewArray(values ...jsValue) jsValue { +func (f v8Scope) NewArray(values ...jsValue) jsArray { // Total hack, v8go doesn't expose Array values, so we polyfill the engine var err error arrayOf, err := f.v8ctx.RunScript("Array.of", "gost-polyfills-array") @@ -209,7 +210,11 @@ func (f v8Scope) NewArray(values ...jsValue) jsValue { if err != nil { panic(err) } - return res + obj, ok := res.AsObject() + if !ok { + panic("not ok") + } + return newV8Array(f.V8ScriptContext, obj.(*v8Object).Object) } else { panic("Array.of is not a function") } diff --git a/scripting/v8engine/script.go b/scripting/v8engine/script.go index d029a274..7429b3c7 100644 --- a/scripting/v8engine/script.go +++ b/scripting/v8engine/script.go @@ -26,22 +26,20 @@ func (s V8Script) Eval() (any, error) { } func v8ValueToGoValue(result *v8go.Value) (any, error) { - if result == nil { + switch { + case result == nil: return nil, nil - } - if result.IsBoolean() { + case result.IsBoolean(): return result.Boolean(), nil - } - if result.IsInt32() { + case result.IsInt32(): return result.Int32(), nil - } - if result.IsString() { + case result.IsNumber(): + return result.Number(), nil + case result.IsString(): return result.String(), nil - } - if result.IsNull() { + case result.IsNull(): return nil, nil - } - if result.IsUndefined() { + case result.IsUndefined(): return nil, nil } if o, err := result.AsObject(); err == nil { diff --git a/scripting/v8engine/script_host_test.go b/scripting/v8engine/script_host_test.go index b41072e8..430c722b 100644 --- a/scripting/v8engine/script_host_test.go +++ b/scripting/v8engine/script_host_test.go @@ -1,10 +1,16 @@ package v8engine import ( + "context" + "log/slog" + "net/http" "testing" + "github.com/gost-dom/browser/html" + "github.com/gost-dom/browser/internal/entity" "github.com/gost-dom/browser/internal/testing/browsertest" . "github.com/gost-dom/browser/internal/testing/gomega-matchers" + "github.com/gost-dom/browser/scripting/internal/js" "github.com/gost-dom/browser/scripting/internal/scripttests" "github.com/onsi/gomega" ) @@ -27,3 +33,19 @@ func TestScriptHostDocumentScriptLoading(t *testing.T) { func TestBasics(t *testing.T) { scripttests.RunBasicSuite(t, assertEngine) } + +type dummyContext struct { + *entity.Entity + ctx context.Context +} + +func (c dummyContext) Context() context.Context { return c.ctx } +func (c dummyContext) HTTPClient() http.Client { return *http.DefaultClient } +func (c dummyContext) LocationHREF() string { return "http://example.com" } +func (c dummyContext) Logger() *slog.Logger { return nil } + +func TestV8Engine(t *testing.T) { + scripttests.RunScriptEngineSuites(t, + func(c js.Configurer[jsTypeParam]) html.ScriptEngine { return newEngine(c) }, + ) +} diff --git a/scripting/v8engine/value.go b/scripting/v8engine/value.go index b9278b0a..cf6d50e6 100644 --- a/scripting/v8engine/value.go +++ b/scripting/v8engine/value.go @@ -14,6 +14,7 @@ type jsValue = js.Value[*v8Value] type jsClass = js.Class[*v8Value] type jsFunction = js.Function[*v8Value] type jsObject = js.Object[*v8Value] +type jsArray = js.Array[*v8Value] type jsError = js.Error[*v8Value] func toV8Value(v jsValue) *v8go.Value { @@ -54,17 +55,20 @@ func (v *v8Value) v8Value() *v8go.Value { return v.Value } -func (v v8Value) String() string { return v.Value.String() } -func (v v8Value) Int32() int32 { return v.Value.Int32() } -func (v v8Value) Uint32() uint32 { return v.Value.Uint32() } -func (v v8Value) Boolean() bool { return v.Value.Boolean() } +func (v v8Value) String() string { return v.Value.String() } +func (v v8Value) Number() float64 { return v.Value.Number() } +func (v v8Value) Int32() int32 { return v.Value.Int32() } +func (v v8Value) Uint32() uint32 { return v.Value.Uint32() } +func (v v8Value) Boolean() bool { return v.Value.Boolean() } func (v v8Value) IsUndefined() bool { return v.Value == nil || v.Value.IsUndefined() } func (v v8Value) IsNull() bool { return v.Value.IsNull() } func (v v8Value) IsBoolean() bool { return v.Value.IsBoolean() } func (v v8Value) IsString() bool { return v.Value.IsString() } +func (v v8Value) IsNumber() bool { return v.Value.IsNumber() } func (v v8Value) IsSymbol() bool { return v.Value.IsSymbol() } func (v v8Value) IsObject() bool { return v.Value.IsObject() } +func (v v8Value) IsArray() bool { return v.Value.IsArray() } func (v v8Value) IsFunction() bool { return v.Value.IsFunction() } func (v v8Value) StrictEquals(