Files
kjol/go/wasmruntime/props_test.go
2026-07-13 15:21:42 -04:00

121 lines
4.1 KiB
Go

//go:build js && wasm
package wasmruntime
import (
"syscall/js"
"testing"
"kjol/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")
}
}