//go:build js && wasm package wasmruntime import ( "syscall/js" "testing" "kjol/wasmruntime/vdom" ) // Run from kjol/go: // // GOOS=js GOARCH=wasm go test -exec="node testdata/domexec.js" ./wasmruntime // Props travel through the vdom as strings, but `checked` is a BOOLEAN property, and in // JavaScript every non-empty string is truthy — so `el.checked = "false"` ticks the box. // A multi-select rendered that way shows every option as selected, forever. // // The reconciler must write a real boolean. func TestBoolPropIsWrittenAsABoolean(t *testing.T) { root := document.Call("createElement", "div") box := vdom.NewRef() unticked := vdom.Input(vdom.WithRef(box), vdom.BoolProp("checked", false)) patchChildren(root, nil, one(unticked)) el, _ := box.Node().(js.Value) if got := el.Get("checked"); got.Type() != js.TypeBoolean { t.Fatalf(`checked is a %v, not a boolean — the string "false" is truthy in JS`, got.Type()) } if el.Get("checked").Bool() { t.Error("BoolProp(false) produced a TICKED checkbox") } } // ...and it must keep tracking the signal. A tick that renders once and then never // changes is the same bug wearing a different hat: the row would look right until you // clicked it. func TestBoolPropUpdatesOnRerender(t *testing.T) { root := document.Call("createElement", "div") box := vdom.NewRef() off := vdom.Input(vdom.WithRef(box), vdom.BoolProp("checked", false)) patchChildren(root, nil, one(off)) el, _ := box.Node().(js.Value) on := vdom.Input(vdom.WithRef(box), vdom.BoolProp("checked", true)) patchChildren(root, one(off), one(on)) if !el.Get("checked").Bool() { t.Fatal("selecting an option did not tick its checkbox") } back := vdom.Input(vdom.WithRef(box), vdom.BoolProp("checked", false)) patchChildren(root, one(on), one(back)) if el.Get("checked").Bool() { t.Error("deselecting an option did not UNTICK its checkbox") } } // A string prop is still a string — the boolean handling must not swallow an input's // value, least of all the literal value "false". func TestStringPropIsUntouched(t *testing.T) { root := document.Call("createElement", "div") r := vdom.NewRef() patchChildren(root, nil, one(vdom.Input(vdom.WithRef(r), vdom.Prop("value", "false")))) el, _ := r.Node().(js.Value) if got := el.Get("value").String(); got != "false" { t.Errorf("value = %q, want the string \"false\"", got) } } // OverflowsX is how the multi-select decides its pills no longer fit. It compares // scrollWidth (all the content) against clientWidth (what is visible) — NOT // getBoundingClientRect, which reports the clipped box and so can never reveal an // overflow. func TestOverflowsX(t *testing.T) { root := document.Call("createElement", "div") cases := []struct { name string client, scroll, want float64 overflows bool }{ {name: "content fits exactly", client: 200, scroll: 200}, {name: "content is wider than the box", client: 200, scroll: 340, overflows: true}, // Sub-pixel layout puts scrollWidth a hair over clientWidth on content that // visually fits; believing that would collapse a field showing two pills. {name: "sub-pixel noise is not an overflow", client: 200, scroll: 200.6}, // A hidden element has no layout at all. That is not an overflow — and reading it // as one is exactly how a collapsed field would get stuck collapsed. {name: "display:none reports nothing", client: 0, scroll: 0}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { r := vdom.NewRef() n := vdom.Div(vdom.WithRef(r)) patchChildren(root, nil, one(n)) el, _ := r.Node().(js.Value) el.Set("clientWidth", tc.client) el.Set("scrollWidth", tc.scroll) if got := OverflowsX(r); got != tc.overflows { t.Errorf("OverflowsX(client=%v scroll=%v) = %v, want %v", tc.client, tc.scroll, got, tc.overflows) } patchChildren(root, one(n), nil) }) } } // An unmounted ref measures nothing rather than panicking — components call the host // API unconditionally, including on the render before their element exists. func TestOverflowsXOnUnmountedRef(t *testing.T) { if OverflowsX(vdom.NewRef()) { t.Error("an unmounted ref should not report an overflow") } }