summaryrefslogtreecommitdiffstats
path: root/src/syscall/js
diff options
context:
space:
mode:
Diffstat (limited to 'src/syscall/js')
-rw-r--r--src/syscall/js/export_test.go9
-rw-r--r--src/syscall/js/func.go96
-rw-r--r--src/syscall/js/js.go583
-rw-r--r--src/syscall/js/js_js.s69
-rw-r--r--src/syscall/js/js_test.go604
5 files changed, 1361 insertions, 0 deletions
diff --git a/src/syscall/js/export_test.go b/src/syscall/js/export_test.go
new file mode 100644
index 0000000..fb61dae
--- /dev/null
+++ b/src/syscall/js/export_test.go
@@ -0,0 +1,9 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build js && wasm
+
+package js
+
+var JSGo = jsGo
diff --git a/src/syscall/js/func.go b/src/syscall/js/func.go
new file mode 100644
index 0000000..cc94972
--- /dev/null
+++ b/src/syscall/js/func.go
@@ -0,0 +1,96 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build js && wasm
+
+package js
+
+import "sync"
+
+var (
+ funcsMu sync.Mutex
+ funcs = make(map[uint32]func(Value, []Value) any)
+ nextFuncID uint32 = 1
+)
+
+// Func is a wrapped Go function to be called by JavaScript.
+type Func struct {
+ Value // the JavaScript function that invokes the Go function
+ id uint32
+}
+
+// FuncOf returns a function to be used by JavaScript.
+//
+// The Go function fn is called with the value of JavaScript's "this" keyword and the
+// arguments of the invocation. The return value of the invocation is
+// the result of the Go function mapped back to JavaScript according to ValueOf.
+//
+// Invoking the wrapped Go function from JavaScript will
+// pause the event loop and spawn a new goroutine.
+// Other wrapped functions which are triggered during a call from Go to JavaScript
+// get executed on the same goroutine.
+//
+// As a consequence, if one wrapped function blocks, JavaScript's event loop
+// is blocked until that function returns. Hence, calling any async JavaScript
+// API, which requires the event loop, like fetch (http.Client), will cause an
+// immediate deadlock. Therefore a blocking function should explicitly start a
+// new goroutine.
+//
+// Func.Release must be called to free up resources when the function will not be invoked any more.
+func FuncOf(fn func(this Value, args []Value) any) Func {
+ funcsMu.Lock()
+ id := nextFuncID
+ nextFuncID++
+ funcs[id] = fn
+ funcsMu.Unlock()
+ return Func{
+ id: id,
+ Value: jsGo.Call("_makeFuncWrapper", id),
+ }
+}
+
+// Release frees up resources allocated for the function.
+// The function must not be invoked after calling Release.
+// It is allowed to call Release while the function is still running.
+func (c Func) Release() {
+ funcsMu.Lock()
+ delete(funcs, c.id)
+ funcsMu.Unlock()
+}
+
+// setEventHandler is defined in the runtime package.
+func setEventHandler(fn func())
+
+func init() {
+ setEventHandler(handleEvent)
+}
+
+func handleEvent() {
+ cb := jsGo.Get("_pendingEvent")
+ if cb.IsNull() {
+ return
+ }
+ jsGo.Set("_pendingEvent", Null())
+
+ id := uint32(cb.Get("id").Int())
+ if id == 0 { // zero indicates deadlock
+ select {}
+ }
+ funcsMu.Lock()
+ f, ok := funcs[id]
+ funcsMu.Unlock()
+ if !ok {
+ Global().Get("console").Call("error", "call to released function")
+ return
+ }
+
+ this := cb.Get("this")
+ argsObj := cb.Get("args")
+ args := make([]Value, argsObj.Length())
+ for i := range args {
+ args[i] = argsObj.Index(i)
+ }
+ result := f(this, args)
+ cb.Set("result", result)
+}
diff --git a/src/syscall/js/js.go b/src/syscall/js/js.go
new file mode 100644
index 0000000..2f4f5ad
--- /dev/null
+++ b/src/syscall/js/js.go
@@ -0,0 +1,583 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build js && wasm
+
+// Package js gives access to the WebAssembly host environment when using the js/wasm architecture.
+// Its API is based on JavaScript semantics.
+//
+// This package is EXPERIMENTAL. Its current scope is only to allow tests to run, but not yet to provide a
+// comprehensive API for users. It is exempt from the Go compatibility promise.
+package js
+
+import (
+ "runtime"
+ "unsafe"
+)
+
+// ref is used to identify a JavaScript value, since the value itself can not be passed to WebAssembly.
+//
+// The JavaScript value "undefined" is represented by the value 0.
+// A JavaScript number (64-bit float, except 0 and NaN) is represented by its IEEE 754 binary representation.
+// All other values are represented as an IEEE 754 binary representation of NaN with bits 0-31 used as
+// an ID and bits 32-34 used to differentiate between string, symbol, function and object.
+type ref uint64
+
+// nanHead are the upper 32 bits of a ref which are set if the value is not encoded as an IEEE 754 number (see above).
+const nanHead = 0x7FF80000
+
+// Value represents a JavaScript value. The zero value is the JavaScript value "undefined".
+// Values can be checked for equality with the Equal method.
+type Value struct {
+ _ [0]func() // uncomparable; to make == not compile
+ ref ref // identifies a JavaScript value, see ref type
+ gcPtr *ref // used to trigger the finalizer when the Value is not referenced any more
+}
+
+const (
+ // the type flags need to be in sync with wasm_exec.js
+ typeFlagNone = iota
+ typeFlagObject
+ typeFlagString
+ typeFlagSymbol
+ typeFlagFunction
+)
+
+func makeValue(r ref) Value {
+ var gcPtr *ref
+ typeFlag := (r >> 32) & 7
+ if (r>>32)&nanHead == nanHead && typeFlag != typeFlagNone {
+ gcPtr = new(ref)
+ *gcPtr = r
+ runtime.SetFinalizer(gcPtr, func(p *ref) {
+ finalizeRef(*p)
+ })
+ }
+
+ return Value{ref: r, gcPtr: gcPtr}
+}
+
+func finalizeRef(r ref)
+
+func predefValue(id uint32, typeFlag byte) Value {
+ return Value{ref: (nanHead|ref(typeFlag))<<32 | ref(id)}
+}
+
+func floatValue(f float64) Value {
+ if f == 0 {
+ return valueZero
+ }
+ if f != f {
+ return valueNaN
+ }
+ return Value{ref: *(*ref)(unsafe.Pointer(&f))}
+}
+
+// Error wraps a JavaScript error.
+type Error struct {
+ // Value is the underlying JavaScript error value.
+ Value
+}
+
+// Error implements the error interface.
+func (e Error) Error() string {
+ return "JavaScript error: " + e.Get("message").String()
+}
+
+var (
+ valueUndefined = Value{ref: 0}
+ valueNaN = predefValue(0, typeFlagNone)
+ valueZero = predefValue(1, typeFlagNone)
+ valueNull = predefValue(2, typeFlagNone)
+ valueTrue = predefValue(3, typeFlagNone)
+ valueFalse = predefValue(4, typeFlagNone)
+ valueGlobal = predefValue(5, typeFlagObject)
+ jsGo = predefValue(6, typeFlagObject) // instance of the Go class in JavaScript
+
+ objectConstructor = valueGlobal.Get("Object")
+ arrayConstructor = valueGlobal.Get("Array")
+)
+
+// Equal reports whether v and w are equal according to JavaScript's === operator.
+func (v Value) Equal(w Value) bool {
+ return v.ref == w.ref && v.ref != valueNaN.ref
+}
+
+// Undefined returns the JavaScript value "undefined".
+func Undefined() Value {
+ return valueUndefined
+}
+
+// IsUndefined reports whether v is the JavaScript value "undefined".
+func (v Value) IsUndefined() bool {
+ return v.ref == valueUndefined.ref
+}
+
+// Null returns the JavaScript value "null".
+func Null() Value {
+ return valueNull
+}
+
+// IsNull reports whether v is the JavaScript value "null".
+func (v Value) IsNull() bool {
+ return v.ref == valueNull.ref
+}
+
+// IsNaN reports whether v is the JavaScript value "NaN".
+func (v Value) IsNaN() bool {
+ return v.ref == valueNaN.ref
+}
+
+// Global returns the JavaScript global object, usually "window" or "global".
+func Global() Value {
+ return valueGlobal
+}
+
+// ValueOf returns x as a JavaScript value:
+//
+// | Go | JavaScript |
+// | ---------------------- | ---------------------- |
+// | js.Value | [its value] |
+// | js.Func | function |
+// | nil | null |
+// | bool | boolean |
+// | integers and floats | number |
+// | string | string |
+// | []interface{} | new array |
+// | map[string]interface{} | new object |
+//
+// Panics if x is not one of the expected types.
+func ValueOf(x any) Value {
+ switch x := x.(type) {
+ case Value:
+ return x
+ case Func:
+ return x.Value
+ case nil:
+ return valueNull
+ case bool:
+ if x {
+ return valueTrue
+ } else {
+ return valueFalse
+ }
+ case int:
+ return floatValue(float64(x))
+ case int8:
+ return floatValue(float64(x))
+ case int16:
+ return floatValue(float64(x))
+ case int32:
+ return floatValue(float64(x))
+ case int64:
+ return floatValue(float64(x))
+ case uint:
+ return floatValue(float64(x))
+ case uint8:
+ return floatValue(float64(x))
+ case uint16:
+ return floatValue(float64(x))
+ case uint32:
+ return floatValue(float64(x))
+ case uint64:
+ return floatValue(float64(x))
+ case uintptr:
+ return floatValue(float64(x))
+ case unsafe.Pointer:
+ return floatValue(float64(uintptr(x)))
+ case float32:
+ return floatValue(float64(x))
+ case float64:
+ return floatValue(x)
+ case string:
+ return makeValue(stringVal(x))
+ case []any:
+ a := arrayConstructor.New(len(x))
+ for i, s := range x {
+ a.SetIndex(i, s)
+ }
+ return a
+ case map[string]any:
+ o := objectConstructor.New()
+ for k, v := range x {
+ o.Set(k, v)
+ }
+ return o
+ default:
+ panic("ValueOf: invalid value")
+ }
+}
+
+func stringVal(x string) ref
+
+// Type represents the JavaScript type of a Value.
+type Type int
+
+const (
+ TypeUndefined Type = iota
+ TypeNull
+ TypeBoolean
+ TypeNumber
+ TypeString
+ TypeSymbol
+ TypeObject
+ TypeFunction
+)
+
+func (t Type) String() string {
+ switch t {
+ case TypeUndefined:
+ return "undefined"
+ case TypeNull:
+ return "null"
+ case TypeBoolean:
+ return "boolean"
+ case TypeNumber:
+ return "number"
+ case TypeString:
+ return "string"
+ case TypeSymbol:
+ return "symbol"
+ case TypeObject:
+ return "object"
+ case TypeFunction:
+ return "function"
+ default:
+ panic("bad type")
+ }
+}
+
+func (t Type) isObject() bool {
+ return t == TypeObject || t == TypeFunction
+}
+
+// Type returns the JavaScript type of the value v. It is similar to JavaScript's typeof operator,
+// except that it returns TypeNull instead of TypeObject for null.
+func (v Value) Type() Type {
+ switch v.ref {
+ case valueUndefined.ref:
+ return TypeUndefined
+ case valueNull.ref:
+ return TypeNull
+ case valueTrue.ref, valueFalse.ref:
+ return TypeBoolean
+ }
+ if v.isNumber() {
+ return TypeNumber
+ }
+ typeFlag := (v.ref >> 32) & 7
+ switch typeFlag {
+ case typeFlagObject:
+ return TypeObject
+ case typeFlagString:
+ return TypeString
+ case typeFlagSymbol:
+ return TypeSymbol
+ case typeFlagFunction:
+ return TypeFunction
+ default:
+ panic("bad type flag")
+ }
+}
+
+// Get returns the JavaScript property p of value v.
+// It panics if v is not a JavaScript object.
+func (v Value) Get(p string) Value {
+ if vType := v.Type(); !vType.isObject() {
+ panic(&ValueError{"Value.Get", vType})
+ }
+ r := makeValue(valueGet(v.ref, p))
+ runtime.KeepAlive(v)
+ return r
+}
+
+func valueGet(v ref, p string) ref
+
+// Set sets the JavaScript property p of value v to ValueOf(x).
+// It panics if v is not a JavaScript object.
+func (v Value) Set(p string, x any) {
+ if vType := v.Type(); !vType.isObject() {
+ panic(&ValueError{"Value.Set", vType})
+ }
+ xv := ValueOf(x)
+ valueSet(v.ref, p, xv.ref)
+ runtime.KeepAlive(v)
+ runtime.KeepAlive(xv)
+}
+
+func valueSet(v ref, p string, x ref)
+
+// Delete deletes the JavaScript property p of value v.
+// It panics if v is not a JavaScript object.
+func (v Value) Delete(p string) {
+ if vType := v.Type(); !vType.isObject() {
+ panic(&ValueError{"Value.Delete", vType})
+ }
+ valueDelete(v.ref, p)
+ runtime.KeepAlive(v)
+}
+
+func valueDelete(v ref, p string)
+
+// Index returns JavaScript index i of value v.
+// It panics if v is not a JavaScript object.
+func (v Value) Index(i int) Value {
+ if vType := v.Type(); !vType.isObject() {
+ panic(&ValueError{"Value.Index", vType})
+ }
+ r := makeValue(valueIndex(v.ref, i))
+ runtime.KeepAlive(v)
+ return r
+}
+
+func valueIndex(v ref, i int) ref
+
+// SetIndex sets the JavaScript index i of value v to ValueOf(x).
+// It panics if v is not a JavaScript object.
+func (v Value) SetIndex(i int, x any) {
+ if vType := v.Type(); !vType.isObject() {
+ panic(&ValueError{"Value.SetIndex", vType})
+ }
+ xv := ValueOf(x)
+ valueSetIndex(v.ref, i, xv.ref)
+ runtime.KeepAlive(v)
+ runtime.KeepAlive(xv)
+}
+
+func valueSetIndex(v ref, i int, x ref)
+
+func makeArgs(args []any) ([]Value, []ref) {
+ argVals := make([]Value, len(args))
+ argRefs := make([]ref, len(args))
+ for i, arg := range args {
+ v := ValueOf(arg)
+ argVals[i] = v
+ argRefs[i] = v.ref
+ }
+ return argVals, argRefs
+}
+
+// Length returns the JavaScript property "length" of v.
+// It panics if v is not a JavaScript object.
+func (v Value) Length() int {
+ if vType := v.Type(); !vType.isObject() {
+ panic(&ValueError{"Value.SetIndex", vType})
+ }
+ r := valueLength(v.ref)
+ runtime.KeepAlive(v)
+ return r
+}
+
+func valueLength(v ref) int
+
+// Call does a JavaScript call to the method m of value v with the given arguments.
+// It panics if v has no method m.
+// The arguments get mapped to JavaScript values according to the ValueOf function.
+func (v Value) Call(m string, args ...any) Value {
+ argVals, argRefs := makeArgs(args)
+ res, ok := valueCall(v.ref, m, argRefs)
+ runtime.KeepAlive(v)
+ runtime.KeepAlive(argVals)
+ if !ok {
+ if vType := v.Type(); !vType.isObject() { // check here to avoid overhead in success case
+ panic(&ValueError{"Value.Call", vType})
+ }
+ if propType := v.Get(m).Type(); propType != TypeFunction {
+ panic("syscall/js: Value.Call: property " + m + " is not a function, got " + propType.String())
+ }
+ panic(Error{makeValue(res)})
+ }
+ return makeValue(res)
+}
+
+func valueCall(v ref, m string, args []ref) (ref, bool)
+
+// Invoke does a JavaScript call of the value v with the given arguments.
+// It panics if v is not a JavaScript function.
+// The arguments get mapped to JavaScript values according to the ValueOf function.
+func (v Value) Invoke(args ...any) Value {
+ argVals, argRefs := makeArgs(args)
+ res, ok := valueInvoke(v.ref, argRefs)
+ runtime.KeepAlive(v)
+ runtime.KeepAlive(argVals)
+ if !ok {
+ if vType := v.Type(); vType != TypeFunction { // check here to avoid overhead in success case
+ panic(&ValueError{"Value.Invoke", vType})
+ }
+ panic(Error{makeValue(res)})
+ }
+ return makeValue(res)
+}
+
+func valueInvoke(v ref, args []ref) (ref, bool)
+
+// New uses JavaScript's "new" operator with value v as constructor and the given arguments.
+// It panics if v is not a JavaScript function.
+// The arguments get mapped to JavaScript values according to the ValueOf function.
+func (v Value) New(args ...any) Value {
+ argVals, argRefs := makeArgs(args)
+ res, ok := valueNew(v.ref, argRefs)
+ runtime.KeepAlive(v)
+ runtime.KeepAlive(argVals)
+ if !ok {
+ if vType := v.Type(); vType != TypeFunction { // check here to avoid overhead in success case
+ panic(&ValueError{"Value.Invoke", vType})
+ }
+ panic(Error{makeValue(res)})
+ }
+ return makeValue(res)
+}
+
+func valueNew(v ref, args []ref) (ref, bool)
+
+func (v Value) isNumber() bool {
+ return v.ref == valueZero.ref ||
+ v.ref == valueNaN.ref ||
+ (v.ref != valueUndefined.ref && (v.ref>>32)&nanHead != nanHead)
+}
+
+func (v Value) float(method string) float64 {
+ if !v.isNumber() {
+ panic(&ValueError{method, v.Type()})
+ }
+ if v.ref == valueZero.ref {
+ return 0
+ }
+ return *(*float64)(unsafe.Pointer(&v.ref))
+}
+
+// Float returns the value v as a float64.
+// It panics if v is not a JavaScript number.
+func (v Value) Float() float64 {
+ return v.float("Value.Float")
+}
+
+// Int returns the value v truncated to an int.
+// It panics if v is not a JavaScript number.
+func (v Value) Int() int {
+ return int(v.float("Value.Int"))
+}
+
+// Bool returns the value v as a bool.
+// It panics if v is not a JavaScript boolean.
+func (v Value) Bool() bool {
+ switch v.ref {
+ case valueTrue.ref:
+ return true
+ case valueFalse.ref:
+ return false
+ default:
+ panic(&ValueError{"Value.Bool", v.Type()})
+ }
+}
+
+// Truthy returns the JavaScript "truthiness" of the value v. In JavaScript,
+// false, 0, "", null, undefined, and NaN are "falsy", and everything else is
+// "truthy". See https://developer.mozilla.org/en-US/docs/Glossary/Truthy.
+func (v Value) Truthy() bool {
+ switch v.Type() {
+ case TypeUndefined, TypeNull:
+ return false
+ case TypeBoolean:
+ return v.Bool()
+ case TypeNumber:
+ return v.ref != valueNaN.ref && v.ref != valueZero.ref
+ case TypeString:
+ return v.String() != ""
+ case TypeSymbol, TypeFunction, TypeObject:
+ return true
+ default:
+ panic("bad type")
+ }
+}
+
+// String returns the value v as a string.
+// String is a special case because of Go's String method convention. Unlike the other getters,
+// it does not panic if v's Type is not TypeString. Instead, it returns a string of the form "<T>"
+// or "<T: V>" where T is v's type and V is a string representation of v's value.
+func (v Value) String() string {
+ switch v.Type() {
+ case TypeString:
+ return jsString(v)
+ case TypeUndefined:
+ return "<undefined>"
+ case TypeNull:
+ return "<null>"
+ case TypeBoolean:
+ return "<boolean: " + jsString(v) + ">"
+ case TypeNumber:
+ return "<number: " + jsString(v) + ">"
+ case TypeSymbol:
+ return "<symbol>"
+ case TypeObject:
+ return "<object>"
+ case TypeFunction:
+ return "<function>"
+ default:
+ panic("bad type")
+ }
+}
+
+func jsString(v Value) string {
+ str, length := valuePrepareString(v.ref)
+ runtime.KeepAlive(v)
+ b := make([]byte, length)
+ valueLoadString(str, b)
+ finalizeRef(str)
+ return string(b)
+}
+
+func valuePrepareString(v ref) (ref, int)
+
+func valueLoadString(v ref, b []byte)
+
+// InstanceOf reports whether v is an instance of type t according to JavaScript's instanceof operator.
+func (v Value) InstanceOf(t Value) bool {
+ r := valueInstanceOf(v.ref, t.ref)
+ runtime.KeepAlive(v)
+ runtime.KeepAlive(t)
+ return r
+}
+
+func valueInstanceOf(v ref, t ref) bool
+
+// A ValueError occurs when a Value method is invoked on
+// a Value that does not support it. Such cases are documented
+// in the description of each method.
+type ValueError struct {
+ Method string
+ Type Type
+}
+
+func (e *ValueError) Error() string {
+ return "syscall/js: call of " + e.Method + " on " + e.Type.String()
+}
+
+// CopyBytesToGo copies bytes from src to dst.
+// It panics if src is not an Uint8Array or Uint8ClampedArray.
+// It returns the number of bytes copied, which will be the minimum of the lengths of src and dst.
+func CopyBytesToGo(dst []byte, src Value) int {
+ n, ok := copyBytesToGo(dst, src.ref)
+ runtime.KeepAlive(src)
+ if !ok {
+ panic("syscall/js: CopyBytesToGo: expected src to be an Uint8Array or Uint8ClampedArray")
+ }
+ return n
+}
+
+func copyBytesToGo(dst []byte, src ref) (int, bool)
+
+// CopyBytesToJS copies bytes from src to dst.
+// It panics if dst is not an Uint8Array or Uint8ClampedArray.
+// It returns the number of bytes copied, which will be the minimum of the lengths of src and dst.
+func CopyBytesToJS(dst Value, src []byte) int {
+ n, ok := copyBytesToJS(dst.ref, src)
+ runtime.KeepAlive(dst)
+ if !ok {
+ panic("syscall/js: CopyBytesToJS: expected dst to be an Uint8Array or Uint8ClampedArray")
+ }
+ return n
+}
+
+func copyBytesToJS(dst ref, src []byte) (int, bool)
diff --git a/src/syscall/js/js_js.s b/src/syscall/js/js_js.s
new file mode 100644
index 0000000..47ad6b8
--- /dev/null
+++ b/src/syscall/js/js_js.s
@@ -0,0 +1,69 @@
+// Copyright 2018 The Go 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 "textflag.h"
+
+TEXT ·finalizeRef(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·stringVal(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·valueGet(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·valueSet(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·valueDelete(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·valueIndex(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·valueSetIndex(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·valueCall(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·valueInvoke(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·valueNew(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·valueLength(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·valuePrepareString(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·valueLoadString(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·valueInstanceOf(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·copyBytesToGo(SB), NOSPLIT, $0
+ CallImport
+ RET
+
+TEXT ·copyBytesToJS(SB), NOSPLIT, $0
+ CallImport
+ RET
diff --git a/src/syscall/js/js_test.go b/src/syscall/js/js_test.go
new file mode 100644
index 0000000..f860a5b
--- /dev/null
+++ b/src/syscall/js/js_test.go
@@ -0,0 +1,604 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build js && wasm
+
+// To run these tests:
+//
+// - Install Node
+// - Add /path/to/go/misc/wasm to your $PATH (so that "go test" can find
+// "go_js_wasm_exec").
+// - GOOS=js GOARCH=wasm go test
+//
+// See -exec in "go help test", and "go help run" for details.
+
+package js_test
+
+import (
+ "fmt"
+ "math"
+ "runtime"
+ "syscall/js"
+ "testing"
+)
+
+var dummys = js.Global().Call("eval", `({
+ someBool: true,
+ someString: "abc\u1234",
+ someInt: 42,
+ someFloat: 42.123,
+ someArray: [41, 42, 43],
+ someDate: new Date(),
+ add: function(a, b) {
+ return a + b;
+ },
+ zero: 0,
+ stringZero: "0",
+ NaN: NaN,
+ emptyObj: {},
+ emptyArray: [],
+ Infinity: Infinity,
+ NegInfinity: -Infinity,
+ objNumber0: new Number(0),
+ objBooleanFalse: new Boolean(false),
+})`)
+
+func TestBool(t *testing.T) {
+ want := true
+ o := dummys.Get("someBool")
+ if got := o.Bool(); got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ dummys.Set("otherBool", want)
+ if got := dummys.Get("otherBool").Bool(); got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if !dummys.Get("someBool").Equal(dummys.Get("someBool")) {
+ t.Errorf("same value not equal")
+ }
+}
+
+func TestString(t *testing.T) {
+ want := "abc\u1234"
+ o := dummys.Get("someString")
+ if got := o.String(); got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ dummys.Set("otherString", want)
+ if got := dummys.Get("otherString").String(); got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if !dummys.Get("someString").Equal(dummys.Get("someString")) {
+ t.Errorf("same value not equal")
+ }
+
+ if got, want := js.Undefined().String(), "<undefined>"; got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if got, want := js.Null().String(), "<null>"; got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if got, want := js.ValueOf(true).String(), "<boolean: true>"; got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if got, want := js.ValueOf(42.5).String(), "<number: 42.5>"; got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if got, want := js.Global().Call("Symbol").String(), "<symbol>"; got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if got, want := js.Global().String(), "<object>"; got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if got, want := js.Global().Get("setTimeout").String(), "<function>"; got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+}
+
+func TestInt(t *testing.T) {
+ want := 42
+ o := dummys.Get("someInt")
+ if got := o.Int(); got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ dummys.Set("otherInt", want)
+ if got := dummys.Get("otherInt").Int(); got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if !dummys.Get("someInt").Equal(dummys.Get("someInt")) {
+ t.Errorf("same value not equal")
+ }
+ if got := dummys.Get("zero").Int(); got != 0 {
+ t.Errorf("got %#v, want %#v", got, 0)
+ }
+}
+
+func TestIntConversion(t *testing.T) {
+ testIntConversion(t, 0)
+ testIntConversion(t, 1)
+ testIntConversion(t, -1)
+ testIntConversion(t, 1<<20)
+ testIntConversion(t, -1<<20)
+ testIntConversion(t, 1<<40)
+ testIntConversion(t, -1<<40)
+ testIntConversion(t, 1<<60)
+ testIntConversion(t, -1<<60)
+}
+
+func testIntConversion(t *testing.T, want int) {
+ if got := js.ValueOf(want).Int(); got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+}
+
+func TestFloat(t *testing.T) {
+ want := 42.123
+ o := dummys.Get("someFloat")
+ if got := o.Float(); got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ dummys.Set("otherFloat", want)
+ if got := dummys.Get("otherFloat").Float(); got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if !dummys.Get("someFloat").Equal(dummys.Get("someFloat")) {
+ t.Errorf("same value not equal")
+ }
+}
+
+func TestObject(t *testing.T) {
+ if !dummys.Get("someArray").Equal(dummys.Get("someArray")) {
+ t.Errorf("same value not equal")
+ }
+
+ // An object and its prototype should not be equal.
+ proto := js.Global().Get("Object").Get("prototype")
+ o := js.Global().Call("eval", "new Object()")
+ if proto.Equal(o) {
+ t.Errorf("object equals to its prototype")
+ }
+}
+
+func TestFrozenObject(t *testing.T) {
+ o := js.Global().Call("eval", "(function () { let o = new Object(); o.field = 5; Object.freeze(o); return o; })()")
+ want := 5
+ if got := o.Get("field").Int(); want != got {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+}
+
+func TestEqual(t *testing.T) {
+ if !dummys.Get("someFloat").Equal(dummys.Get("someFloat")) {
+ t.Errorf("same float is not equal")
+ }
+ if !dummys.Get("emptyObj").Equal(dummys.Get("emptyObj")) {
+ t.Errorf("same object is not equal")
+ }
+ if dummys.Get("someFloat").Equal(dummys.Get("someInt")) {
+ t.Errorf("different values are not unequal")
+ }
+}
+
+func TestNaN(t *testing.T) {
+ if !dummys.Get("NaN").IsNaN() {
+ t.Errorf("JS NaN is not NaN")
+ }
+ if !js.ValueOf(math.NaN()).IsNaN() {
+ t.Errorf("Go NaN is not NaN")
+ }
+ if dummys.Get("NaN").Equal(dummys.Get("NaN")) {
+ t.Errorf("NaN is equal to NaN")
+ }
+}
+
+func TestUndefined(t *testing.T) {
+ if !js.Undefined().IsUndefined() {
+ t.Errorf("undefined is not undefined")
+ }
+ if !js.Undefined().Equal(js.Undefined()) {
+ t.Errorf("undefined is not equal to undefined")
+ }
+ if dummys.IsUndefined() {
+ t.Errorf("object is undefined")
+ }
+ if js.Undefined().IsNull() {
+ t.Errorf("undefined is null")
+ }
+ if dummys.Set("test", js.Undefined()); !dummys.Get("test").IsUndefined() {
+ t.Errorf("could not set undefined")
+ }
+}
+
+func TestNull(t *testing.T) {
+ if !js.Null().IsNull() {
+ t.Errorf("null is not null")
+ }
+ if !js.Null().Equal(js.Null()) {
+ t.Errorf("null is not equal to null")
+ }
+ if dummys.IsNull() {
+ t.Errorf("object is null")
+ }
+ if js.Null().IsUndefined() {
+ t.Errorf("null is undefined")
+ }
+ if dummys.Set("test", js.Null()); !dummys.Get("test").IsNull() {
+ t.Errorf("could not set null")
+ }
+ if dummys.Set("test", nil); !dummys.Get("test").IsNull() {
+ t.Errorf("could not set nil")
+ }
+}
+
+func TestLength(t *testing.T) {
+ if got := dummys.Get("someArray").Length(); got != 3 {
+ t.Errorf("got %#v, want %#v", got, 3)
+ }
+}
+
+func TestGet(t *testing.T) {
+ // positive cases get tested per type
+
+ expectValueError(t, func() {
+ dummys.Get("zero").Get("badField")
+ })
+}
+
+func TestSet(t *testing.T) {
+ // positive cases get tested per type
+
+ expectValueError(t, func() {
+ dummys.Get("zero").Set("badField", 42)
+ })
+}
+
+func TestDelete(t *testing.T) {
+ dummys.Set("test", 42)
+ dummys.Delete("test")
+ if dummys.Call("hasOwnProperty", "test").Bool() {
+ t.Errorf("property still exists")
+ }
+
+ expectValueError(t, func() {
+ dummys.Get("zero").Delete("badField")
+ })
+}
+
+func TestIndex(t *testing.T) {
+ if got := dummys.Get("someArray").Index(1).Int(); got != 42 {
+ t.Errorf("got %#v, want %#v", got, 42)
+ }
+
+ expectValueError(t, func() {
+ dummys.Get("zero").Index(1)
+ })
+}
+
+func TestSetIndex(t *testing.T) {
+ dummys.Get("someArray").SetIndex(2, 99)
+ if got := dummys.Get("someArray").Index(2).Int(); got != 99 {
+ t.Errorf("got %#v, want %#v", got, 99)
+ }
+
+ expectValueError(t, func() {
+ dummys.Get("zero").SetIndex(2, 99)
+ })
+}
+
+func TestCall(t *testing.T) {
+ var i int64 = 40
+ if got := dummys.Call("add", i, 2).Int(); got != 42 {
+ t.Errorf("got %#v, want %#v", got, 42)
+ }
+ if got := dummys.Call("add", js.Global().Call("eval", "40"), 2).Int(); got != 42 {
+ t.Errorf("got %#v, want %#v", got, 42)
+ }
+
+ expectPanic(t, func() {
+ dummys.Call("zero")
+ })
+ expectValueError(t, func() {
+ dummys.Get("zero").Call("badMethod")
+ })
+}
+
+func TestInvoke(t *testing.T) {
+ var i int64 = 40
+ if got := dummys.Get("add").Invoke(i, 2).Int(); got != 42 {
+ t.Errorf("got %#v, want %#v", got, 42)
+ }
+
+ expectValueError(t, func() {
+ dummys.Get("zero").Invoke()
+ })
+}
+
+func TestNew(t *testing.T) {
+ if got := js.Global().Get("Array").New(42).Length(); got != 42 {
+ t.Errorf("got %#v, want %#v", got, 42)
+ }
+
+ expectValueError(t, func() {
+ dummys.Get("zero").New()
+ })
+}
+
+func TestInstanceOf(t *testing.T) {
+ someArray := js.Global().Get("Array").New()
+ if got, want := someArray.InstanceOf(js.Global().Get("Array")), true; got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if got, want := someArray.InstanceOf(js.Global().Get("Function")), false; got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+}
+
+func TestType(t *testing.T) {
+ if got, want := js.Undefined().Type(), js.TypeUndefined; got != want {
+ t.Errorf("got %s, want %s", got, want)
+ }
+ if got, want := js.Null().Type(), js.TypeNull; got != want {
+ t.Errorf("got %s, want %s", got, want)
+ }
+ if got, want := js.ValueOf(true).Type(), js.TypeBoolean; got != want {
+ t.Errorf("got %s, want %s", got, want)
+ }
+ if got, want := js.ValueOf(0).Type(), js.TypeNumber; got != want {
+ t.Errorf("got %s, want %s", got, want)
+ }
+ if got, want := js.ValueOf(42).Type(), js.TypeNumber; got != want {
+ t.Errorf("got %s, want %s", got, want)
+ }
+ if got, want := js.ValueOf("test").Type(), js.TypeString; got != want {
+ t.Errorf("got %s, want %s", got, want)
+ }
+ if got, want := js.Global().Get("Symbol").Invoke("test").Type(), js.TypeSymbol; got != want {
+ t.Errorf("got %s, want %s", got, want)
+ }
+ if got, want := js.Global().Get("Array").New().Type(), js.TypeObject; got != want {
+ t.Errorf("got %s, want %s", got, want)
+ }
+ if got, want := js.Global().Get("Array").Type(), js.TypeFunction; got != want {
+ t.Errorf("got %s, want %s", got, want)
+ }
+}
+
+type object = map[string]any
+type array = []any
+
+func TestValueOf(t *testing.T) {
+ a := js.ValueOf(array{0, array{0, 42, 0}, 0})
+ if got := a.Index(1).Index(1).Int(); got != 42 {
+ t.Errorf("got %v, want %v", got, 42)
+ }
+
+ o := js.ValueOf(object{"x": object{"y": 42}})
+ if got := o.Get("x").Get("y").Int(); got != 42 {
+ t.Errorf("got %v, want %v", got, 42)
+ }
+}
+
+func TestZeroValue(t *testing.T) {
+ var v js.Value
+ if !v.IsUndefined() {
+ t.Error("zero js.Value is not js.Undefined()")
+ }
+}
+
+func TestFuncOf(t *testing.T) {
+ c := make(chan struct{})
+ cb := js.FuncOf(func(this js.Value, args []js.Value) any {
+ if got := args[0].Int(); got != 42 {
+ t.Errorf("got %#v, want %#v", got, 42)
+ }
+ c <- struct{}{}
+ return nil
+ })
+ defer cb.Release()
+ js.Global().Call("setTimeout", cb, 0, 42)
+ <-c
+}
+
+func TestInvokeFunction(t *testing.T) {
+ called := false
+ cb := js.FuncOf(func(this js.Value, args []js.Value) any {
+ cb2 := js.FuncOf(func(this js.Value, args []js.Value) any {
+ called = true
+ return 42
+ })
+ defer cb2.Release()
+ return cb2.Invoke()
+ })
+ defer cb.Release()
+ if got := cb.Invoke().Int(); got != 42 {
+ t.Errorf("got %#v, want %#v", got, 42)
+ }
+ if !called {
+ t.Error("function not called")
+ }
+}
+
+func TestInterleavedFunctions(t *testing.T) {
+ c1 := make(chan struct{})
+ c2 := make(chan struct{})
+
+ js.Global().Get("setTimeout").Invoke(js.FuncOf(func(this js.Value, args []js.Value) any {
+ c1 <- struct{}{}
+ <-c2
+ return nil
+ }), 0)
+
+ <-c1
+ c2 <- struct{}{}
+ // this goroutine is running, but the callback of setTimeout did not return yet, invoke another function now
+ f := js.FuncOf(func(this js.Value, args []js.Value) any {
+ return nil
+ })
+ f.Invoke()
+}
+
+func ExampleFuncOf() {
+ var cb js.Func
+ cb = js.FuncOf(func(this js.Value, args []js.Value) any {
+ fmt.Println("button clicked")
+ cb.Release() // release the function if the button will not be clicked again
+ return nil
+ })
+ js.Global().Get("document").Call("getElementById", "myButton").Call("addEventListener", "click", cb)
+}
+
+// See
+// - https://developer.mozilla.org/en-US/docs/Glossary/Truthy
+// - https://stackoverflow.com/questions/19839952/all-falsey-values-in-javascript/19839953#19839953
+// - http://www.ecma-international.org/ecma-262/5.1/#sec-9.2
+func TestTruthy(t *testing.T) {
+ want := true
+ for _, key := range []string{
+ "someBool", "someString", "someInt", "someFloat", "someArray", "someDate",
+ "stringZero", // "0" is truthy
+ "add", // functions are truthy
+ "emptyObj", "emptyArray", "Infinity", "NegInfinity",
+ // All objects are truthy, even if they're Number(0) or Boolean(false).
+ "objNumber0", "objBooleanFalse",
+ } {
+ if got := dummys.Get(key).Truthy(); got != want {
+ t.Errorf("%s: got %#v, want %#v", key, got, want)
+ }
+ }
+
+ want = false
+ if got := dummys.Get("zero").Truthy(); got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if got := dummys.Get("NaN").Truthy(); got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if got := js.ValueOf("").Truthy(); got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if got := js.Null().Truthy(); got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+ if got := js.Undefined().Truthy(); got != want {
+ t.Errorf("got %#v, want %#v", got, want)
+ }
+}
+
+func expectValueError(t *testing.T, fn func()) {
+ defer func() {
+ err := recover()
+ if _, ok := err.(*js.ValueError); !ok {
+ t.Errorf("expected *js.ValueError, got %T", err)
+ }
+ }()
+ fn()
+}
+
+func expectPanic(t *testing.T, fn func()) {
+ defer func() {
+ err := recover()
+ if err == nil {
+ t.Errorf("expected panic")
+ }
+ }()
+ fn()
+}
+
+var copyTests = []struct {
+ srcLen int
+ dstLen int
+ copyLen int
+}{
+ {5, 3, 3},
+ {3, 5, 3},
+ {0, 0, 0},
+}
+
+func TestCopyBytesToGo(t *testing.T) {
+ for _, tt := range copyTests {
+ t.Run(fmt.Sprintf("%d-to-%d", tt.srcLen, tt.dstLen), func(t *testing.T) {
+ src := js.Global().Get("Uint8Array").New(tt.srcLen)
+ if tt.srcLen >= 2 {
+ src.SetIndex(1, 42)
+ }
+ dst := make([]byte, tt.dstLen)
+
+ if got, want := js.CopyBytesToGo(dst, src), tt.copyLen; got != want {
+ t.Errorf("copied %d, want %d", got, want)
+ }
+ if tt.dstLen >= 2 {
+ if got, want := int(dst[1]), 42; got != want {
+ t.Errorf("got %d, want %d", got, want)
+ }
+ }
+ })
+ }
+}
+
+func TestCopyBytesToJS(t *testing.T) {
+ for _, tt := range copyTests {
+ t.Run(fmt.Sprintf("%d-to-%d", tt.srcLen, tt.dstLen), func(t *testing.T) {
+ src := make([]byte, tt.srcLen)
+ if tt.srcLen >= 2 {
+ src[1] = 42
+ }
+ dst := js.Global().Get("Uint8Array").New(tt.dstLen)
+
+ if got, want := js.CopyBytesToJS(dst, src), tt.copyLen; got != want {
+ t.Errorf("copied %d, want %d", got, want)
+ }
+ if tt.dstLen >= 2 {
+ if got, want := dst.Index(1).Int(), 42; got != want {
+ t.Errorf("got %d, want %d", got, want)
+ }
+ }
+ })
+ }
+}
+
+func TestGarbageCollection(t *testing.T) {
+ before := js.JSGo.Get("_values").Length()
+ for i := 0; i < 1000; i++ {
+ _ = js.Global().Get("Object").New().Call("toString").String()
+ runtime.GC()
+ }
+ after := js.JSGo.Get("_values").Length()
+ if after-before > 500 {
+ t.Errorf("garbage collection ineffective")
+ }
+}
+
+// BenchmarkDOM is a simple benchmark which emulates a webapp making DOM operations.
+// It creates a div, and sets its id. Then searches by that id and sets some data.
+// Finally it removes that div.
+func BenchmarkDOM(b *testing.B) {
+ document := js.Global().Get("document")
+ if document.IsUndefined() {
+ b.Skip("Not a browser environment. Skipping.")
+ }
+ const data = "someString"
+ for i := 0; i < b.N; i++ {
+ div := document.Call("createElement", "div")
+ div.Call("setAttribute", "id", "myDiv")
+ document.Get("body").Call("appendChild", div)
+ myDiv := document.Call("getElementById", "myDiv")
+ myDiv.Set("innerHTML", data)
+
+ if got, want := myDiv.Get("innerHTML").String(), data; got != want {
+ b.Errorf("got %s, want %s", got, want)
+ }
+ document.Get("body").Call("removeChild", div)
+ }
+}
+
+func TestGlobal(t *testing.T) {
+ ident := js.FuncOf(func(this js.Value, args []js.Value) any {
+ return args[0]
+ })
+ defer ident.Release()
+
+ if got := ident.Invoke(js.Global()); !got.Equal(js.Global()) {
+ t.Errorf("got %#v, want %#v", got, js.Global())
+ }
+}