- Export Signal type (signal → Signal) so subpackages can reference it - Expose viewport signals as public fields (CenterLng, CenterLat, Zoom, Bearing, Pitch) for .Text() display and .Bind() usage - Add signal-backed marker positions (LngSignal/LatSignal) with data-effect reactivity for server push and dragend writeback - Add event system (MapEvent, OnClick, OnLayerClick, OnMouseMove, OnContextMenu) using hidden inputs + action triggers - Add Source interface replacing type-switch, with RawSource escape hatch - Add CameraOptions for FlyTo/EaseTo/JumpTo/FitBounds/Stop - Add Control interface with NavigationControl, ScaleControl, GeolocateControl, FullscreenControl - Expand Options with interaction toggles, MaxBounds, and Extra map - Add effectspike example to validate data-effect with server-pushed signals - Update maplibre example to showcase all new features
191 lines
4.6 KiB
Go
191 lines
4.6 KiB
Go
package via
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/ryanhamamura/via/h"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestComputedBasic(t *testing.T) {
|
|
v := New()
|
|
var cs *computedSignal
|
|
v.Page("/", func(c *Context) {
|
|
sig1 := c.Signal("hello")
|
|
sig2 := c.Signal("world")
|
|
cs = c.Computed(func() string {
|
|
return sig1.String() + " " + sig2.String()
|
|
})
|
|
c.View(func() h.H { return h.Div() })
|
|
})
|
|
assert.Equal(t, "hello world", cs.String())
|
|
}
|
|
|
|
func TestComputedReactivity(t *testing.T) {
|
|
v := New()
|
|
var cs *computedSignal
|
|
var sig1 *Signal
|
|
v.Page("/", func(c *Context) {
|
|
sig1 = c.Signal("a")
|
|
sig2 := c.Signal("b")
|
|
cs = c.Computed(func() string {
|
|
return sig1.String() + sig2.String()
|
|
})
|
|
c.View(func() h.H { return h.Div() })
|
|
})
|
|
assert.Equal(t, "ab", cs.String())
|
|
sig1.SetValue("x")
|
|
assert.Equal(t, "xb", cs.String())
|
|
}
|
|
|
|
func TestComputedInt(t *testing.T) {
|
|
v := New()
|
|
var cs *computedSignal
|
|
v.Page("/", func(c *Context) {
|
|
sig := c.Signal(21)
|
|
cs = c.Computed(func() string {
|
|
return fmt.Sprintf("%d", sig.Int()*2)
|
|
})
|
|
c.View(func() h.H { return h.Div() })
|
|
})
|
|
assert.Equal(t, 42, cs.Int())
|
|
}
|
|
|
|
func TestComputedBool(t *testing.T) {
|
|
v := New()
|
|
var cs *computedSignal
|
|
v.Page("/", func(c *Context) {
|
|
sig := c.Signal("true")
|
|
cs = c.Computed(func() string {
|
|
return sig.String()
|
|
})
|
|
c.View(func() h.H { return h.Div() })
|
|
})
|
|
assert.True(t, cs.Bool())
|
|
}
|
|
|
|
func TestComputedText(t *testing.T) {
|
|
v := New()
|
|
var cs *computedSignal
|
|
v.Page("/", func(c *Context) {
|
|
cs = c.Computed(func() string { return "hi" })
|
|
c.View(func() h.H { return h.Div() })
|
|
})
|
|
|
|
var buf bytes.Buffer
|
|
err := cs.Text().Render(&buf)
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, buf.String(), `data-text="$`+cs.ID()+`"`)
|
|
}
|
|
|
|
func TestComputedChangeDetection(t *testing.T) {
|
|
v := New()
|
|
var ctx *Context
|
|
var sig *Signal
|
|
v.Page("/", func(c *Context) {
|
|
ctx = c
|
|
sig = c.Signal("a")
|
|
c.Computed(func() string {
|
|
return sig.String() + "!"
|
|
})
|
|
c.View(func() h.H { return h.Div() })
|
|
})
|
|
|
|
// First patch includes computed (changed=true from init)
|
|
patch1 := ctx.prepareSignalsForPatch()
|
|
assert.NotEmpty(t, patch1)
|
|
|
|
// Second patch: nothing changed, computed should not be included
|
|
patch2 := ctx.prepareSignalsForPatch()
|
|
// Regular signal still has changed=true (not reset in prepareSignalsForPatch),
|
|
// but computed should not appear since its value didn't change.
|
|
hasComputed := false
|
|
ctx.signals.Range(func(_, value any) bool {
|
|
if cs, ok := value.(*computedSignal); ok {
|
|
_, inPatch := patch2[cs.ID()]
|
|
hasComputed = inPatch
|
|
}
|
|
return true
|
|
})
|
|
assert.False(t, hasComputed)
|
|
|
|
// After changing dependency, computed should reappear
|
|
sig.SetValue("b")
|
|
patch3 := ctx.prepareSignalsForPatch()
|
|
found := false
|
|
ctx.signals.Range(func(_, value any) bool {
|
|
if cs, ok := value.(*computedSignal); ok {
|
|
if v, ok := patch3[cs.ID()]; ok {
|
|
assert.Equal(t, "b!", v)
|
|
found = true
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
assert.True(t, found)
|
|
}
|
|
|
|
func TestComputedInComponent(t *testing.T) {
|
|
v := New()
|
|
var cs *computedSignal
|
|
var parentCtx *Context
|
|
v.Page("/", func(c *Context) {
|
|
parentCtx = c
|
|
c.Component(func(comp *Context) {
|
|
sig := comp.Signal("via")
|
|
cs = comp.Computed(func() string {
|
|
return "hello " + sig.String()
|
|
})
|
|
comp.View(func() h.H { return h.Div() })
|
|
})
|
|
c.View(func() h.H { return h.Div() })
|
|
})
|
|
|
|
assert.Equal(t, "hello via", cs.String())
|
|
// Verify it's stored on the parent page context
|
|
found := false
|
|
parentCtx.signals.Range(func(_, value any) bool {
|
|
if stored, ok := value.(*computedSignal); ok && stored.ID() == cs.ID() {
|
|
found = true
|
|
}
|
|
return true
|
|
})
|
|
assert.True(t, found)
|
|
}
|
|
|
|
func TestComputedIsReadOnly(t *testing.T) {
|
|
// Compile-time guarantee: *computedSignal has no Bind() or SetValue() methods.
|
|
// This test exists as documentation — if someone adds those methods, the
|
|
// interface assertion below will need updating and serve as a reminder.
|
|
var cs interface{} = &computedSignal{}
|
|
type writable interface {
|
|
SetValue(any)
|
|
}
|
|
type bindable interface {
|
|
Bind() h.H
|
|
}
|
|
_, isWritable := cs.(writable)
|
|
_, isBindable := cs.(bindable)
|
|
assert.False(t, isWritable, "computedSignal must not have SetValue")
|
|
assert.False(t, isBindable, "computedSignal must not have Bind")
|
|
}
|
|
|
|
func TestComputedInjectSignalsSkips(t *testing.T) {
|
|
v := New()
|
|
var ctx *Context
|
|
var cs *computedSignal
|
|
v.Page("/", func(c *Context) {
|
|
ctx = c
|
|
cs = c.Computed(func() string { return "fixed" })
|
|
c.View(func() h.H { return h.Div() })
|
|
})
|
|
|
|
// Simulate browser sending back a value for the computed signal — should be ignored
|
|
ctx.injectSignals(map[string]any{
|
|
cs.ID(): "injected",
|
|
})
|
|
assert.Equal(t, "fixed", cs.String())
|
|
}
|