Files
kjol/go/wasmruntime/host_test.go

215 lines
7.2 KiB
Go

//go:build js && wasm
package wasmruntime
import (
"strings"
"syscall/js"
"testing"
"kjol/wasmruntime/vdom"
)
// Run from kjol/go:
//
// GOOS=js GOARCH=wasm go test -exec="node testdata/domexec.js" ./wasmruntime
// A Ref must be attached when the element is created, survive a diff that reuses
// the element, and be nil again once the element is gone — otherwise measurement
// code silently reads a stale node.
func TestRefLifecycle(t *testing.T) {
root := document.Call("createElement", "div")
panel := vdom.NewRef()
if panel.Mounted() {
t.Fatal("a fresh ref should not be mounted")
}
a := vdom.El("div", vdom.WithRef(panel), vdom.Attr("class", "one"))
patchChildren(root, nil, one(a))
if !panel.Mounted() {
t.Fatal("ref was not attached on create")
}
created, _ := panel.Node().(js.Value)
// A re-render builds a fresh VNode carrying the same *Ref; the diff adopts the
// existing element, so the ref must still point at it.
b := vdom.El("div", vdom.WithRef(panel), vdom.Attr("class", "two"))
patchChildren(root, one(a), one(b))
if !panel.Mounted() {
t.Fatal("ref was detached by a re-render that reused the element")
}
if adopted, _ := panel.Node().(js.Value); !adopted.Equal(created) {
t.Error("ref points at a different node after a reusing diff")
}
// Removing the element must clear the ref.
patchChildren(root, one(b), nil)
if panel.Mounted() {
t.Error("ref still points at a removed element")
}
}
// A tag change replaces the element. createDOM runs before release (replaceChild
// needs both nodes), so a naive detach-on-release would nil the ref that was just
// pointed at the NEW element.
func TestRefSurvivesTagChange(t *testing.T) {
root := document.Call("createElement", "div")
r := vdom.NewRef()
a := vdom.El("div", vdom.WithRef(r))
patchChildren(root, nil, one(a))
b := vdom.El("span", vdom.WithRef(r))
patchChildren(root, one(a), one(b))
if !r.Mounted() {
t.Fatal("ref was cleared when the element changed tag")
}
if got := Measure(r); got != (Rect{}) { // shim reports a zero rect by default
_ = got
}
if !strings.Contains(root.Get("innerHTML").String(), "<span") {
t.Error("replacement element was not rendered")
}
}
// Measure must read the element's box, and must return the zero Rect (not a box at
// the origin) for an unmounted ref, so positioning code can tell "not laid out yet"
// from "genuinely at 0,0".
func TestMeasure(t *testing.T) {
if got := Measure(vdom.NewRef()); !got.Empty() {
t.Errorf("unmounted ref measured as %+v, want the zero Rect", got)
}
root := document.Call("createElement", "div")
r := vdom.NewRef()
patchChildren(root, nil, one(vdom.El("div", vdom.WithRef(r))))
n, _ := r.Node().(js.Value)
rect := n.Get("rect")
rect.Set("left", 10)
rect.Set("top", 20)
rect.Set("width", 100)
rect.Set("height", 40)
got := Measure(r)
want := Rect{X: 10, Y: 20, Width: 100, Height: 40}
if got != want {
t.Fatalf("Measure = %+v, want %+v", got, want)
}
if got.Right() != 110 || got.Bottom() != 60 || got.CenterX() != 60 {
t.Errorf("derived edges wrong: right=%v bottom=%v centerX=%v", got.Right(), got.Bottom(), got.CenterX())
}
if got.Empty() {
t.Error("a measured element reported Empty()")
}
}
// SetStyle writes straight to the DOM, deliberately bypassing the vdom — that is
// what keeps per-frame repositioning from re-rendering the whole app.
func TestSetStyleBypassesVDOM(t *testing.T) {
root := document.Call("createElement", "div")
r := vdom.NewRef()
patchChildren(root, nil, one(vdom.El("div", vdom.WithRef(r))))
SetStyle(r, "top", "42px")
SetStyle(r, "left", "7px")
if got := root.Get("innerHTML").String(); !strings.Contains(got, "left: 7px; top: 42px") {
t.Fatalf("styles not written: %s", got)
}
RemoveStyle(r, "top")
if got := root.Get("innerHTML").String(); strings.Contains(got, "top:") {
t.Errorf("style not removed: %s", got)
}
SetStyle(vdom.NewRef(), "top", "1px") // unmounted: must not panic
}
// The whole point of the portal: children mount into document.body, NOT into the
// parent — so a panel can escape an ancestor's overflow:hidden / transform.
func TestPortalMountsToBody(t *testing.T) {
root := document.Call("createElement", "div")
body := document.Get("body")
before := body.Get("childNodes").Get("length").Int()
tree := vdom.El("div", vdom.Attr("class", "clipped"),
vdom.Portal(vdom.El("div", vdom.Attr("class", "panel"), vdom.Text("floating"))),
)
patchChildren(root, nil, one(tree))
if got := root.Get("innerHTML").String(); strings.Contains(got, "floating") {
t.Errorf("portal content rendered in-flow instead of at body level:\n%s", got)
}
if got := root.Get("innerHTML").String(); !strings.Contains(got, "data-portal") {
t.Errorf("portal placeholder missing from the tree (sibling indexes will drift):\n%s", got)
}
if body.Get("childNodes").Get("length").Int() != before+1 {
t.Fatal("portal container was not appended to body")
}
container := body.Get("childNodes").Index(before)
if got := container.Get("innerHTML").String(); !strings.Contains(got, "floating") {
t.Fatalf("portal children not in the body container: %s", got)
}
// Unmounting the portal must take the body-level container with it, or the
// panel outlives the component that opened it.
patchChildren(root, one(tree), nil)
if body.Get("childNodes").Get("length").Int() != before {
t.Error("portal container leaked into body after unmount")
}
}
// A portal's children must diff against the body container across re-renders, not
// be recreated or appended to the placeholder.
func TestPortalPatchesChildrenInPlace(t *testing.T) {
root := document.Call("createElement", "div")
body := document.Get("body")
before := body.Get("childNodes").Get("length").Int()
a := vdom.El("div", vdom.Portal(vdom.El("p", vdom.Text("first"))))
patchChildren(root, nil, one(a))
container := body.Get("childNodes").Index(before)
b := vdom.El("div", vdom.Portal(vdom.El("p", vdom.Text("second"))))
patchChildren(root, one(a), one(b))
if body.Get("childNodes").Get("length").Int() != before+1 {
t.Fatal("re-render created a second portal container")
}
got := container.Get("innerHTML").String()
if !strings.Contains(got, "second") || strings.Contains(got, "first") {
t.Errorf("portal children not patched in place: %s", got)
}
patchChildren(root, one(b), nil)
}
// Outside-click detection is built on these two.
func TestContainsAndClosestAttr(t *testing.T) {
root := document.Call("createElement", "div")
panel := vdom.NewRef()
inner := vdom.NewRef()
tree := vdom.El("div", vdom.Attr("data-floating-id", "menu-1"), vdom.WithRef(panel),
vdom.El("button", vdom.WithRef(inner), vdom.Text("item")),
)
patchChildren(root, nil, one(tree))
target := inner.Node()
if !Contains(panel, target) {
t.Error("Contains missed a descendant")
}
other := vdom.NewRef()
patchChildren(root, nil, one(vdom.El("div", vdom.WithRef(other))))
if Contains(other, target) {
t.Error("Contains matched an unrelated element")
}
id, ok := ClosestAttr(target, "[data-floating-id]", "data-floating-id")
if !ok || id != "menu-1" {
t.Errorf("ClosestAttr = %q, %v; want \"menu-1\", true", id, ok)
}
if _, ok := ClosestAttr(nil, "[data-floating-id]", "data-floating-id"); ok {
t.Error("ClosestAttr should report not-found for a nil target")
}
}