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