1// Copyright 2018 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5//go:build js && wasm
6
7// To run these tests:
8//
9// - Install Node
10// - Add /path/to/go/misc/wasm to your $PATH (so that "go test" can find
11//   "go_js_wasm_exec").
12// - GOOS=js GOARCH=wasm go test
13//
14// See -exec in "go help test", and "go help run" for details.
15
16package js_test
17
18import (
19	"fmt"
20	"math"
21	"runtime"
22	"syscall/js"
23	"testing"
24)
25
26var dummys = js.Global().Call("eval", `({
27	someBool: true,
28	someString: "abc\u1234",
29	someInt: 42,
30	someFloat: 42.123,
31	someArray: [41, 42, 43],
32	someDate: new Date(),
33	add: function(a, b) {
34		return a + b;
35	},
36	zero: 0,
37	stringZero: "0",
38	NaN: NaN,
39	emptyObj: {},
40	emptyArray: [],
41	Infinity: Infinity,
42	NegInfinity: -Infinity,
43	objNumber0: new Number(0),
44	objBooleanFalse: new Boolean(false),
45})`)
46
47//go:wasmimport _gotest add
48func testAdd(uint32, uint32) uint32
49
50func TestWasmImport(t *testing.T) {
51	a := uint32(3)
52	b := uint32(5)
53	want := a + b
54	if got := testAdd(a, b); got != want {
55		t.Errorf("got %v, want %v", got, want)
56	}
57}
58
59func TestBool(t *testing.T) {
60	want := true
61	o := dummys.Get("someBool")
62	if got := o.Bool(); got != want {
63		t.Errorf("got %#v, want %#v", got, want)
64	}
65	dummys.Set("otherBool", want)
66	if got := dummys.Get("otherBool").Bool(); got != want {
67		t.Errorf("got %#v, want %#v", got, want)
68	}
69	if !dummys.Get("someBool").Equal(dummys.Get("someBool")) {
70		t.Errorf("same value not equal")
71	}
72}
73
74func TestString(t *testing.T) {
75	want := "abc\u1234"
76	o := dummys.Get("someString")
77	if got := o.String(); got != want {
78		t.Errorf("got %#v, want %#v", got, want)
79	}
80	dummys.Set("otherString", want)
81	if got := dummys.Get("otherString").String(); got != want {
82		t.Errorf("got %#v, want %#v", got, want)
83	}
84	if !dummys.Get("someString").Equal(dummys.Get("someString")) {
85		t.Errorf("same value not equal")
86	}
87
88	if got, want := js.Undefined().String(), "<undefined>"; got != want {
89		t.Errorf("got %#v, want %#v", got, want)
90	}
91	if got, want := js.Null().String(), "<null>"; got != want {
92		t.Errorf("got %#v, want %#v", got, want)
93	}
94	if got, want := js.ValueOf(true).String(), "<boolean: true>"; got != want {
95		t.Errorf("got %#v, want %#v", got, want)
96	}
97	if got, want := js.ValueOf(42.5).String(), "<number: 42.5>"; got != want {
98		t.Errorf("got %#v, want %#v", got, want)
99	}
100	if got, want := js.Global().Call("Symbol").String(), "<symbol>"; got != want {
101		t.Errorf("got %#v, want %#v", got, want)
102	}
103	if got, want := js.Global().String(), "<object>"; got != want {
104		t.Errorf("got %#v, want %#v", got, want)
105	}
106	if got, want := js.Global().Get("setTimeout").String(), "<function>"; got != want {
107		t.Errorf("got %#v, want %#v", got, want)
108	}
109}
110
111func TestInt(t *testing.T) {
112	want := 42
113	o := dummys.Get("someInt")
114	if got := o.Int(); got != want {
115		t.Errorf("got %#v, want %#v", got, want)
116	}
117	dummys.Set("otherInt", want)
118	if got := dummys.Get("otherInt").Int(); got != want {
119		t.Errorf("got %#v, want %#v", got, want)
120	}
121	if !dummys.Get("someInt").Equal(dummys.Get("someInt")) {
122		t.Errorf("same value not equal")
123	}
124	if got := dummys.Get("zero").Int(); got != 0 {
125		t.Errorf("got %#v, want %#v", got, 0)
126	}
127}
128
129func TestIntConversion(t *testing.T) {
130	testIntConversion(t, 0)
131	testIntConversion(t, 1)
132	testIntConversion(t, -1)
133	testIntConversion(t, 1<<20)
134	testIntConversion(t, -1<<20)
135	testIntConversion(t, 1<<40)
136	testIntConversion(t, -1<<40)
137	testIntConversion(t, 1<<60)
138	testIntConversion(t, -1<<60)
139}
140
141func testIntConversion(t *testing.T, want int) {
142	if got := js.ValueOf(want).Int(); got != want {
143		t.Errorf("got %#v, want %#v", got, want)
144	}
145}
146
147func TestFloat(t *testing.T) {
148	want := 42.123
149	o := dummys.Get("someFloat")
150	if got := o.Float(); got != want {
151		t.Errorf("got %#v, want %#v", got, want)
152	}
153	dummys.Set("otherFloat", want)
154	if got := dummys.Get("otherFloat").Float(); got != want {
155		t.Errorf("got %#v, want %#v", got, want)
156	}
157	if !dummys.Get("someFloat").Equal(dummys.Get("someFloat")) {
158		t.Errorf("same value not equal")
159	}
160}
161
162func TestObject(t *testing.T) {
163	if !dummys.Get("someArray").Equal(dummys.Get("someArray")) {
164		t.Errorf("same value not equal")
165	}
166
167	// An object and its prototype should not be equal.
168	proto := js.Global().Get("Object").Get("prototype")
169	o := js.Global().Call("eval", "new Object()")
170	if proto.Equal(o) {
171		t.Errorf("object equals to its prototype")
172	}
173}
174
175func TestFrozenObject(t *testing.T) {
176	o := js.Global().Call("eval", "(function () { let o = new Object(); o.field = 5; Object.freeze(o); return o; })()")
177	want := 5
178	if got := o.Get("field").Int(); want != got {
179		t.Errorf("got %#v, want %#v", got, want)
180	}
181}
182
183func TestEqual(t *testing.T) {
184	if !dummys.Get("someFloat").Equal(dummys.Get("someFloat")) {
185		t.Errorf("same float is not equal")
186	}
187	if !dummys.Get("emptyObj").Equal(dummys.Get("emptyObj")) {
188		t.Errorf("same object is not equal")
189	}
190	if dummys.Get("someFloat").Equal(dummys.Get("someInt")) {
191		t.Errorf("different values are not unequal")
192	}
193}
194
195func TestNaN(t *testing.T) {
196	if !dummys.Get("NaN").IsNaN() {
197		t.Errorf("JS NaN is not NaN")
198	}
199	if !js.ValueOf(math.NaN()).IsNaN() {
200		t.Errorf("Go NaN is not NaN")
201	}
202	if dummys.Get("NaN").Equal(dummys.Get("NaN")) {
203		t.Errorf("NaN is equal to NaN")
204	}
205}
206
207func TestUndefined(t *testing.T) {
208	if !js.Undefined().IsUndefined() {
209		t.Errorf("undefined is not undefined")
210	}
211	if !js.Undefined().Equal(js.Undefined()) {
212		t.Errorf("undefined is not equal to undefined")
213	}
214	if dummys.IsUndefined() {
215		t.Errorf("object is undefined")
216	}
217	if js.Undefined().IsNull() {
218		t.Errorf("undefined is null")
219	}
220	if dummys.Set("test", js.Undefined()); !dummys.Get("test").IsUndefined() {
221		t.Errorf("could not set undefined")
222	}
223}
224
225func TestNull(t *testing.T) {
226	if !js.Null().IsNull() {
227		t.Errorf("null is not null")
228	}
229	if !js.Null().Equal(js.Null()) {
230		t.Errorf("null is not equal to null")
231	}
232	if dummys.IsNull() {
233		t.Errorf("object is null")
234	}
235	if js.Null().IsUndefined() {
236		t.Errorf("null is undefined")
237	}
238	if dummys.Set("test", js.Null()); !dummys.Get("test").IsNull() {
239		t.Errorf("could not set null")
240	}
241	if dummys.Set("test", nil); !dummys.Get("test").IsNull() {
242		t.Errorf("could not set nil")
243	}
244}
245
246func TestLength(t *testing.T) {
247	if got := dummys.Get("someArray").Length(); got != 3 {
248		t.Errorf("got %#v, want %#v", got, 3)
249	}
250}
251
252func TestGet(t *testing.T) {
253	// positive cases get tested per type
254
255	expectValueError(t, func() {
256		dummys.Get("zero").Get("badField")
257	})
258}
259
260func TestSet(t *testing.T) {
261	// positive cases get tested per type
262
263	expectValueError(t, func() {
264		dummys.Get("zero").Set("badField", 42)
265	})
266}
267
268func TestDelete(t *testing.T) {
269	dummys.Set("test", 42)
270	dummys.Delete("test")
271	if dummys.Call("hasOwnProperty", "test").Bool() {
272		t.Errorf("property still exists")
273	}
274
275	expectValueError(t, func() {
276		dummys.Get("zero").Delete("badField")
277	})
278}
279
280func TestIndex(t *testing.T) {
281	if got := dummys.Get("someArray").Index(1).Int(); got != 42 {
282		t.Errorf("got %#v, want %#v", got, 42)
283	}
284
285	expectValueError(t, func() {
286		dummys.Get("zero").Index(1)
287	})
288}
289
290func TestSetIndex(t *testing.T) {
291	dummys.Get("someArray").SetIndex(2, 99)
292	if got := dummys.Get("someArray").Index(2).Int(); got != 99 {
293		t.Errorf("got %#v, want %#v", got, 99)
294	}
295
296	expectValueError(t, func() {
297		dummys.Get("zero").SetIndex(2, 99)
298	})
299}
300
301func TestCall(t *testing.T) {
302	var i int64 = 40
303	if got := dummys.Call("add", i, 2).Int(); got != 42 {
304		t.Errorf("got %#v, want %#v", got, 42)
305	}
306	if got := dummys.Call("add", js.Global().Call("eval", "40"), 2).Int(); got != 42 {
307		t.Errorf("got %#v, want %#v", got, 42)
308	}
309
310	expectPanic(t, func() {
311		dummys.Call("zero")
312	})
313	expectValueError(t, func() {
314		dummys.Get("zero").Call("badMethod")
315	})
316}
317
318func TestInvoke(t *testing.T) {
319	var i int64 = 40
320	if got := dummys.Get("add").Invoke(i, 2).Int(); got != 42 {
321		t.Errorf("got %#v, want %#v", got, 42)
322	}
323
324	expectValueError(t, func() {
325		dummys.Get("zero").Invoke()
326	})
327}
328
329func TestNew(t *testing.T) {
330	if got := js.Global().Get("Array").New(42).Length(); got != 42 {
331		t.Errorf("got %#v, want %#v", got, 42)
332	}
333
334	expectValueError(t, func() {
335		dummys.Get("zero").New()
336	})
337}
338
339func TestInstanceOf(t *testing.T) {
340	someArray := js.Global().Get("Array").New()
341	if got, want := someArray.InstanceOf(js.Global().Get("Array")), true; got != want {
342		t.Errorf("got %#v, want %#v", got, want)
343	}
344	if got, want := someArray.InstanceOf(js.Global().Get("Function")), false; got != want {
345		t.Errorf("got %#v, want %#v", got, want)
346	}
347}
348
349func TestType(t *testing.T) {
350	if got, want := js.Undefined().Type(), js.TypeUndefined; got != want {
351		t.Errorf("got %s, want %s", got, want)
352	}
353	if got, want := js.Null().Type(), js.TypeNull; got != want {
354		t.Errorf("got %s, want %s", got, want)
355	}
356	if got, want := js.ValueOf(true).Type(), js.TypeBoolean; got != want {
357		t.Errorf("got %s, want %s", got, want)
358	}
359	if got, want := js.ValueOf(0).Type(), js.TypeNumber; got != want {
360		t.Errorf("got %s, want %s", got, want)
361	}
362	if got, want := js.ValueOf(42).Type(), js.TypeNumber; got != want {
363		t.Errorf("got %s, want %s", got, want)
364	}
365	if got, want := js.ValueOf("test").Type(), js.TypeString; got != want {
366		t.Errorf("got %s, want %s", got, want)
367	}
368	if got, want := js.Global().Get("Symbol").Invoke("test").Type(), js.TypeSymbol; got != want {
369		t.Errorf("got %s, want %s", got, want)
370	}
371	if got, want := js.Global().Get("Array").New().Type(), js.TypeObject; got != want {
372		t.Errorf("got %s, want %s", got, want)
373	}
374	if got, want := js.Global().Get("Array").Type(), js.TypeFunction; got != want {
375		t.Errorf("got %s, want %s", got, want)
376	}
377}
378
379type object = map[string]any
380type array = []any
381
382func TestValueOf(t *testing.T) {
383	a := js.ValueOf(array{0, array{0, 42, 0}, 0})
384	if got := a.Index(1).Index(1).Int(); got != 42 {
385		t.Errorf("got %v, want %v", got, 42)
386	}
387
388	o := js.ValueOf(object{"x": object{"y": 42}})
389	if got := o.Get("x").Get("y").Int(); got != 42 {
390		t.Errorf("got %v, want %v", got, 42)
391	}
392}
393
394func TestZeroValue(t *testing.T) {
395	var v js.Value
396	if !v.IsUndefined() {
397		t.Error("zero js.Value is not js.Undefined()")
398	}
399}
400
401func TestFuncOf(t *testing.T) {
402	c := make(chan struct{})
403	cb := js.FuncOf(func(this js.Value, args []js.Value) any {
404		if got := args[0].Int(); got != 42 {
405			t.Errorf("got %#v, want %#v", got, 42)
406		}
407		c <- struct{}{}
408		return nil
409	})
410	defer cb.Release()
411	js.Global().Call("setTimeout", cb, 0, 42)
412	<-c
413}
414
415func TestInvokeFunction(t *testing.T) {
416	called := false
417	cb := js.FuncOf(func(this js.Value, args []js.Value) any {
418		cb2 := js.FuncOf(func(this js.Value, args []js.Value) any {
419			called = true
420			return 42
421		})
422		defer cb2.Release()
423		return cb2.Invoke()
424	})
425	defer cb.Release()
426	if got := cb.Invoke().Int(); got != 42 {
427		t.Errorf("got %#v, want %#v", got, 42)
428	}
429	if !called {
430		t.Error("function not called")
431	}
432}
433
434func TestInterleavedFunctions(t *testing.T) {
435	c1 := make(chan struct{})
436	c2 := make(chan struct{})
437
438	js.Global().Get("setTimeout").Invoke(js.FuncOf(func(this js.Value, args []js.Value) any {
439		c1 <- struct{}{}
440		<-c2
441		return nil
442	}), 0)
443
444	<-c1
445	c2 <- struct{}{}
446	// this goroutine is running, but the callback of setTimeout did not return yet, invoke another function now
447	f := js.FuncOf(func(this js.Value, args []js.Value) any {
448		return nil
449	})
450	f.Invoke()
451}
452
453func ExampleFuncOf() {
454	var cb js.Func
455	cb = js.FuncOf(func(this js.Value, args []js.Value) any {
456		fmt.Println("button clicked")
457		cb.Release() // release the function if the button will not be clicked again
458		return nil
459	})
460	js.Global().Get("document").Call("getElementById", "myButton").Call("addEventListener", "click", cb)
461}
462
463// See
464// - https://developer.mozilla.org/en-US/docs/Glossary/Truthy
465// - https://stackoverflow.com/questions/19839952/all-falsey-values-in-javascript/19839953#19839953
466// - http://www.ecma-international.org/ecma-262/5.1/#sec-9.2
467func TestTruthy(t *testing.T) {
468	want := true
469	for _, key := range []string{
470		"someBool", "someString", "someInt", "someFloat", "someArray", "someDate",
471		"stringZero", // "0" is truthy
472		"add",        // functions are truthy
473		"emptyObj", "emptyArray", "Infinity", "NegInfinity",
474		// All objects are truthy, even if they're Number(0) or Boolean(false).
475		"objNumber0", "objBooleanFalse",
476	} {
477		if got := dummys.Get(key).Truthy(); got != want {
478			t.Errorf("%s: got %#v, want %#v", key, got, want)
479		}
480	}
481
482	want = false
483	if got := dummys.Get("zero").Truthy(); got != want {
484		t.Errorf("got %#v, want %#v", got, want)
485	}
486	if got := dummys.Get("NaN").Truthy(); got != want {
487		t.Errorf("got %#v, want %#v", got, want)
488	}
489	if got := js.ValueOf("").Truthy(); got != want {
490		t.Errorf("got %#v, want %#v", got, want)
491	}
492	if got := js.Null().Truthy(); got != want {
493		t.Errorf("got %#v, want %#v", got, want)
494	}
495	if got := js.Undefined().Truthy(); got != want {
496		t.Errorf("got %#v, want %#v", got, want)
497	}
498}
499
500func expectValueError(t *testing.T, fn func()) {
501	defer func() {
502		err := recover()
503		if _, ok := err.(*js.ValueError); !ok {
504			t.Errorf("expected *js.ValueError, got %T", err)
505		}
506	}()
507	fn()
508}
509
510func expectPanic(t *testing.T, fn func()) {
511	defer func() {
512		err := recover()
513		if err == nil {
514			t.Errorf("expected panic")
515		}
516	}()
517	fn()
518}
519
520var copyTests = []struct {
521	srcLen  int
522	dstLen  int
523	copyLen int
524}{
525	{5, 3, 3},
526	{3, 5, 3},
527	{0, 0, 0},
528}
529
530func TestCopyBytesToGo(t *testing.T) {
531	for _, tt := range copyTests {
532		t.Run(fmt.Sprintf("%d-to-%d", tt.srcLen, tt.dstLen), func(t *testing.T) {
533			src := js.Global().Get("Uint8Array").New(tt.srcLen)
534			if tt.srcLen >= 2 {
535				src.SetIndex(1, 42)
536			}
537			dst := make([]byte, tt.dstLen)
538
539			if got, want := js.CopyBytesToGo(dst, src), tt.copyLen; got != want {
540				t.Errorf("copied %d, want %d", got, want)
541			}
542			if tt.dstLen >= 2 {
543				if got, want := int(dst[1]), 42; got != want {
544					t.Errorf("got %d, want %d", got, want)
545				}
546			}
547		})
548	}
549}
550
551func TestCopyBytesToJS(t *testing.T) {
552	for _, tt := range copyTests {
553		t.Run(fmt.Sprintf("%d-to-%d", tt.srcLen, tt.dstLen), func(t *testing.T) {
554			src := make([]byte, tt.srcLen)
555			if tt.srcLen >= 2 {
556				src[1] = 42
557			}
558			dst := js.Global().Get("Uint8Array").New(tt.dstLen)
559
560			if got, want := js.CopyBytesToJS(dst, src), tt.copyLen; got != want {
561				t.Errorf("copied %d, want %d", got, want)
562			}
563			if tt.dstLen >= 2 {
564				if got, want := dst.Index(1).Int(), 42; got != want {
565					t.Errorf("got %d, want %d", got, want)
566				}
567			}
568		})
569	}
570}
571
572func TestGarbageCollection(t *testing.T) {
573	before := js.JSGo.Get("_values").Length()
574	for i := 0; i < 1000; i++ {
575		_ = js.Global().Get("Object").New().Call("toString").String()
576		runtime.GC()
577	}
578	after := js.JSGo.Get("_values").Length()
579	if after-before > 500 {
580		t.Errorf("garbage collection ineffective")
581	}
582}
583
584// This table is used for allocation tests. We expect a specific allocation
585// behavior to be seen, depending on the number of arguments applied to various
586// JavaScript functions.
587// Note: All JavaScript functions return a JavaScript array, which will cause
588// one allocation to be created to track the Value.gcPtr for the Value finalizer.
589var allocTests = []struct {
590	argLen  int // The number of arguments to use for the syscall
591	expected int // The expected number of allocations
592}{
593	// For less than or equal to 16 arguments, we expect 1 alloction:
594	// - makeValue new(ref)
595	{0,  1},
596	{2,  1},
597	{15, 1},
598	{16, 1},
599	// For greater than 16 arguments, we expect 3 alloction:
600	// - makeValue: new(ref)
601	// - makeArgSlices: argVals = make([]Value, size)
602	// - makeArgSlices: argRefs = make([]ref, size)
603	{17, 3},
604	{32, 3},
605	{42, 3},
606}
607
608// TestCallAllocations ensures the correct allocation profile for Value.Call
609func TestCallAllocations(t *testing.T) {
610	for _, test := range allocTests {
611		args := make([]any, test.argLen)
612
613		tmpArray := js.Global().Get("Array").New(0)
614		numAllocs := testing.AllocsPerRun(100, func() {
615			tmpArray.Call("concat", args...)
616		});
617
618		if numAllocs != float64(test.expected) {
619			t.Errorf("got numAllocs %#v, want %#v", numAllocs, test.expected)
620		}
621	}
622}
623
624// TestInvokeAllocations ensures the correct allocation profile for Value.Invoke
625func TestInvokeAllocations(t *testing.T) {
626	for _, test := range allocTests {
627		args := make([]any, test.argLen)
628
629		tmpArray := js.Global().Get("Array").New(0)
630		concatFunc := tmpArray.Get("concat").Call("bind", tmpArray)
631		numAllocs := testing.AllocsPerRun(100, func() {
632			concatFunc.Invoke(args...)
633		});
634
635		if numAllocs != float64(test.expected) {
636			t.Errorf("got numAllocs %#v, want %#v", numAllocs, test.expected)
637		}
638	}
639}
640
641// TestNewAllocations ensures the correct allocation profile for Value.New
642func TestNewAllocations(t *testing.T) {
643	arrayConstructor := js.Global().Get("Array")
644
645	for _, test := range allocTests {
646		args := make([]any, test.argLen)
647
648		numAllocs := testing.AllocsPerRun(100, func() {
649			arrayConstructor.New(args...)
650		});
651
652		if numAllocs != float64(test.expected) {
653			t.Errorf("got numAllocs %#v, want %#v", numAllocs, test.expected)
654		}
655	}
656}
657
658// BenchmarkDOM is a simple benchmark which emulates a webapp making DOM operations.
659// It creates a div, and sets its id. Then searches by that id and sets some data.
660// Finally it removes that div.
661func BenchmarkDOM(b *testing.B) {
662	document := js.Global().Get("document")
663	if document.IsUndefined() {
664		b.Skip("Not a browser environment. Skipping.")
665	}
666	const data = "someString"
667	for i := 0; i < b.N; i++ {
668		div := document.Call("createElement", "div")
669		div.Call("setAttribute", "id", "myDiv")
670		document.Get("body").Call("appendChild", div)
671		myDiv := document.Call("getElementById", "myDiv")
672		myDiv.Set("innerHTML", data)
673
674		if got, want := myDiv.Get("innerHTML").String(), data; got != want {
675			b.Errorf("got %s, want %s", got, want)
676		}
677		document.Get("body").Call("removeChild", div)
678	}
679}
680
681func TestGlobal(t *testing.T) {
682	ident := js.FuncOf(func(this js.Value, args []js.Value) any {
683		return args[0]
684	})
685	defer ident.Release()
686
687	if got := ident.Invoke(js.Global()); !got.Equal(js.Global()) {
688		t.Errorf("got %#v, want %#v", got, js.Global())
689	}
690}
691