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