457 lines
13 KiB
Go
457 lines
13 KiB
Go
package via
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ryanhamamura/via/h"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestPageRoute(t *testing.T) {
|
|
v := New()
|
|
v.Page("/", func(c *Context) {
|
|
c.View(func() h.H {
|
|
return h.Div(h.Text("Hello Via!"))
|
|
})
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Hello Via!")
|
|
assert.Contains(t, w.Body.String(), "<!doctype html>")
|
|
}
|
|
|
|
func TestDatastarJS(t *testing.T) {
|
|
v := New()
|
|
v.Page("/", func(c *Context) {
|
|
c.View(func() h.H { return h.Div() })
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/_datastar.js", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "application/javascript", w.Header().Get("Content-Type"))
|
|
assert.Contains(t, w.Body.String(), "🖕JS_DS🚀")
|
|
}
|
|
|
|
func TestCustomDatastarContent(t *testing.T) {
|
|
customScript := []byte("// Custom Datastar Script")
|
|
v := New()
|
|
v.Config(Options{
|
|
DatastarContent: customScript,
|
|
})
|
|
v.Page("/", func(c *Context) {
|
|
c.View(func() h.H { return h.Div() })
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/_datastar.js", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "application/javascript", w.Header().Get("Content-Type"))
|
|
assert.Contains(t, w.Body.String(), "Custom Datastar Script")
|
|
}
|
|
|
|
func TestCustomDatastarPath(t *testing.T) {
|
|
v := New()
|
|
v.Config(Options{
|
|
DatastarPath: "/assets/datastar.js",
|
|
})
|
|
v.Page("/test", func(c *Context) {
|
|
c.View(func() h.H { return h.Div() })
|
|
})
|
|
|
|
// Custom path should serve the script
|
|
req := httptest.NewRequest("GET", "/assets/datastar.js", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "application/javascript", w.Header().Get("Content-Type"))
|
|
assert.Contains(t, w.Body.String(), "🖕JS_DS🚀")
|
|
|
|
// Page should reference the custom path in script tag
|
|
req2 := httptest.NewRequest("GET", "/test", nil)
|
|
w2 := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w2, req2)
|
|
assert.Contains(t, w2.Body.String(), `src="/assets/datastar.js"`)
|
|
}
|
|
|
|
func TestSignal(t *testing.T) {
|
|
var sig *Signal
|
|
v := New()
|
|
v.Page("/", func(c *Context) {
|
|
sig = c.Signal("test")
|
|
c.View(func() h.H { return h.Div() })
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, "test", sig.String())
|
|
}
|
|
|
|
func TestAction(t *testing.T) {
|
|
var trigger *actionTrigger
|
|
var sig *Signal
|
|
v := New()
|
|
v.Page("/", func(c *Context) {
|
|
trigger = c.Action(func() {})
|
|
sig = c.Signal("value")
|
|
c.View(func() h.H {
|
|
return h.Div(
|
|
h.Button(trigger.OnClick()),
|
|
h.Input(trigger.OnChange()),
|
|
h.Input(trigger.OnKeyDown("Enter")),
|
|
h.Button(trigger.OnClick(WithSignal(sig, "test"))),
|
|
h.Button(trigger.OnClick(WithSignalInt(sig, 42))),
|
|
)
|
|
})
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
body := w.Body.String()
|
|
assert.Contains(t, body, "data-on:click")
|
|
assert.Contains(t, body, "data-on:change")
|
|
assert.Contains(t, body, "data-on:keydown")
|
|
assert.Contains(t, body, "/_action/")
|
|
}
|
|
|
|
func TestEventTypes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
attr string
|
|
buildEl func(trigger *actionTrigger) h.H
|
|
}{
|
|
{"OnSubmit", "data-on:submit", func(tr *actionTrigger) h.H { return h.Form(tr.OnSubmit()) }},
|
|
{"OnInput", "data-on:input", func(tr *actionTrigger) h.H { return h.Input(tr.OnInput()) }},
|
|
{"OnFocus", "data-on:focus", func(tr *actionTrigger) h.H { return h.Input(tr.OnFocus()) }},
|
|
{"OnBlur", "data-on:blur", func(tr *actionTrigger) h.H { return h.Input(tr.OnBlur()) }},
|
|
{"OnMouseEnter", "data-on:mouseenter", func(tr *actionTrigger) h.H { return h.Div(tr.OnMouseEnter()) }},
|
|
{"OnMouseLeave", "data-on:mouseleave", func(tr *actionTrigger) h.H { return h.Div(tr.OnMouseLeave()) }},
|
|
{"OnScroll", "data-on:scroll", func(tr *actionTrigger) h.H { return h.Div(tr.OnScroll()) }},
|
|
{"OnDblClick", "data-on:dblclick", func(tr *actionTrigger) h.H { return h.Div(tr.OnDblClick()) }},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var trigger *actionTrigger
|
|
v := New()
|
|
v.Page("/", func(c *Context) {
|
|
trigger = c.Action(func() {})
|
|
c.View(func() h.H { return tt.buildEl(trigger) })
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
body := w.Body.String()
|
|
assert.Contains(t, body, tt.attr)
|
|
assert.Contains(t, body, "/_action/"+trigger.id)
|
|
})
|
|
}
|
|
|
|
t.Run("WithSignal", func(t *testing.T) {
|
|
var trigger *actionTrigger
|
|
var sig *Signal
|
|
v := New()
|
|
v.Page("/", func(c *Context) {
|
|
trigger = c.Action(func() {})
|
|
sig = c.Signal("val")
|
|
c.View(func() h.H {
|
|
return h.Div(trigger.OnDblClick(WithSignal(sig, "x")))
|
|
})
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
body := w.Body.String()
|
|
assert.Contains(t, body, "data-on:dblclick")
|
|
assert.Contains(t, body, "$"+sig.ID()+"='x'")
|
|
})
|
|
}
|
|
|
|
func TestOnKeyDownWithWindow(t *testing.T) {
|
|
var trigger *actionTrigger
|
|
v := New()
|
|
v.Page("/", func(c *Context) {
|
|
trigger = c.Action(func() {})
|
|
c.View(func() h.H {
|
|
return h.Div(trigger.OnKeyDown("Enter", WithWindow()))
|
|
})
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
body := w.Body.String()
|
|
assert.Contains(t, body, "data-on:keydown__window")
|
|
assert.Contains(t, body, "evt.key==='Enter'")
|
|
}
|
|
|
|
func TestOnKeyDownMap(t *testing.T) {
|
|
t.Run("multiple bindings with different actions", func(t *testing.T) {
|
|
var move, shoot *actionTrigger
|
|
var dir *Signal
|
|
v := New()
|
|
v.Page("/", func(c *Context) {
|
|
dir = c.Signal("none")
|
|
move = c.Action(func() {})
|
|
shoot = c.Action(func() {})
|
|
c.View(func() h.H {
|
|
return h.Div(
|
|
OnKeyDownMap(
|
|
KeyBind("w", move, WithSignal(dir, "up")),
|
|
KeyBind("ArrowUp", move, WithSignal(dir, "up"), WithPreventDefault()),
|
|
KeyBind(" ", shoot, WithPreventDefault()),
|
|
),
|
|
)
|
|
})
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
body := w.Body.String()
|
|
|
|
// Single attribute, window-scoped
|
|
assert.Contains(t, body, "data-on:keydown__window")
|
|
|
|
// Key dispatching
|
|
assert.Contains(t, body, "evt.key==='w'")
|
|
assert.Contains(t, body, "evt.key==='ArrowUp'")
|
|
assert.Contains(t, body, "evt.key===' '")
|
|
|
|
// Different actions referenced
|
|
assert.Contains(t, body, "/_action/"+move.id)
|
|
assert.Contains(t, body, "/_action/"+shoot.id)
|
|
|
|
// preventDefault only on ArrowUp and space branches
|
|
assert.Contains(t, body, "evt.key==='ArrowUp' ? (evt.preventDefault()")
|
|
assert.Contains(t, body, "evt.key===' ' ? (evt.preventDefault()")
|
|
|
|
// 'w' branch should NOT have preventDefault
|
|
assert.NotContains(t, body, "evt.key==='w' ? (evt.preventDefault()")
|
|
})
|
|
|
|
t.Run("WithSignal per binding", func(t *testing.T) {
|
|
var move *actionTrigger
|
|
var dir *Signal
|
|
v := New()
|
|
v.Page("/", func(c *Context) {
|
|
dir = c.Signal("none")
|
|
move = c.Action(func() {})
|
|
c.View(func() h.H {
|
|
return h.Div(
|
|
OnKeyDownMap(
|
|
KeyBind("w", move, WithSignal(dir, "up")),
|
|
KeyBind("s", move, WithSignal(dir, "down")),
|
|
),
|
|
)
|
|
})
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
body := w.Body.String()
|
|
|
|
assert.Contains(t, body, "$"+dir.ID()+"='up'")
|
|
assert.Contains(t, body, "$"+dir.ID()+"='down'")
|
|
})
|
|
|
|
t.Run("empty bindings returns nil", func(t *testing.T) {
|
|
result := OnKeyDownMap()
|
|
assert.Nil(t, result)
|
|
})
|
|
}
|
|
|
|
func TestFormatDuration(t *testing.T) {
|
|
tests := []struct {
|
|
d time.Duration
|
|
want string
|
|
}{
|
|
{200 * time.Millisecond, "200ms"},
|
|
{1 * time.Second, "1000ms"},
|
|
{50 * time.Millisecond, "50ms"},
|
|
}
|
|
for _, tt := range tests {
|
|
assert.Equal(t, tt.want, formatDuration(tt.d))
|
|
}
|
|
}
|
|
|
|
func TestBuildAttrKey(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
event string
|
|
opts triggerOpts
|
|
want string
|
|
}{
|
|
{"bare event", "click", triggerOpts{}, "on:click"},
|
|
{"debounce only", "change", triggerOpts{debounce: 200 * time.Millisecond}, "on:change__debounce.200ms"},
|
|
{"throttle only", "scroll", triggerOpts{throttle: 100 * time.Millisecond}, "on:scroll__throttle.100ms"},
|
|
{"window only", "keydown", triggerOpts{window: true}, "on:keydown__window"},
|
|
{"debounce + window", "input", triggerOpts{debounce: 300 * time.Millisecond, window: true}, "on:input__debounce.300ms__window"},
|
|
{"throttle + window", "scroll", triggerOpts{throttle: 500 * time.Millisecond, window: true}, "on:scroll__throttle.500ms__window"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, buildAttrKey(tt.event, &tt.opts))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWithDebounce(t *testing.T) {
|
|
var trigger *actionTrigger
|
|
v := New()
|
|
v.Page("/", func(c *Context) {
|
|
trigger = c.Action(func() {})
|
|
c.View(func() h.H {
|
|
return h.Button(trigger.OnClick(WithDebounce(300 * time.Millisecond)))
|
|
})
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
body := w.Body.String()
|
|
assert.Contains(t, body, "data-on:click__debounce.300ms")
|
|
assert.Contains(t, body, "/_action/"+trigger.id)
|
|
}
|
|
|
|
func TestWithThrottle(t *testing.T) {
|
|
var trigger *actionTrigger
|
|
v := New()
|
|
v.Page("/", func(c *Context) {
|
|
trigger = c.Action(func() {})
|
|
c.View(func() h.H {
|
|
return h.Div(trigger.OnScroll(WithThrottle(100 * time.Millisecond)))
|
|
})
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
body := w.Body.String()
|
|
assert.Contains(t, body, "data-on:scroll__throttle.100ms")
|
|
assert.Contains(t, body, "/_action/"+trigger.id)
|
|
}
|
|
|
|
func TestWithDebounceOnChange(t *testing.T) {
|
|
var trigger *actionTrigger
|
|
v := New()
|
|
v.Page("/", func(c *Context) {
|
|
trigger = c.Action(func() {})
|
|
c.View(func() h.H {
|
|
return h.Input(trigger.OnChange(WithDebounce(200 * time.Millisecond)))
|
|
})
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
body := w.Body.String()
|
|
assert.Contains(t, body, "data-on:change__debounce.200ms")
|
|
assert.Contains(t, body, "/_action/"+trigger.id)
|
|
}
|
|
|
|
func TestDebounceWithWindow(t *testing.T) {
|
|
var trigger *actionTrigger
|
|
v := New()
|
|
v.Page("/", func(c *Context) {
|
|
trigger = c.Action(func() {})
|
|
c.View(func() h.H {
|
|
return h.Div(trigger.OnKeyDown("Enter", WithDebounce(150*time.Millisecond), WithWindow()))
|
|
})
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
v.mux.ServeHTTP(w, req)
|
|
body := w.Body.String()
|
|
assert.Contains(t, body, "data-on:keydown__debounce.150ms__window")
|
|
}
|
|
|
|
func TestConfig(t *testing.T) {
|
|
v := New()
|
|
v.Config(Options{DocumentTitle: "Test"})
|
|
assert.Equal(t, "Test", v.cfg.DocumentTitle)
|
|
}
|
|
|
|
func TestPage_PanicsOnNoView(t *testing.T) {
|
|
assert.Panics(t, func() {
|
|
v := New()
|
|
v.Page("/", func(c *Context) {})
|
|
})
|
|
}
|
|
|
|
func TestCleanupCtxIdempotent(t *testing.T) {
|
|
v := New()
|
|
c := newContext("idempotent-1", "/", v)
|
|
v.registerCtx(c)
|
|
|
|
assert.NotPanics(t, func() {
|
|
v.cleanupCtx(c)
|
|
v.cleanupCtx(c)
|
|
})
|
|
|
|
_, err := v.getCtx("idempotent-1")
|
|
assert.Error(t, err, "context should be removed after cleanup")
|
|
}
|
|
|
|
func TestDevModeRemovePersistedFix(t *testing.T) {
|
|
v := New()
|
|
v.cfg.DevMode = true
|
|
|
|
dir := filepath.Join(t.TempDir(), ".via", "devmode")
|
|
p := filepath.Join(dir, "ctx.json")
|
|
assert.NoError(t, os.MkdirAll(dir, 0755))
|
|
|
|
// Write a persisted context
|
|
ctxRegMap := map[string]string{"test-ctx-1": "/"}
|
|
f, err := os.Create(p)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, json.NewEncoder(f).Encode(ctxRegMap))
|
|
f.Close()
|
|
|
|
// Patch devModeRemovePersisted to use our temp path by calling it
|
|
// directly — we need to override the path. Instead, test via the
|
|
// actual function by temporarily changing the working dir.
|
|
origDir, _ := os.Getwd()
|
|
assert.NoError(t, os.Chdir(t.TempDir()))
|
|
defer os.Chdir(origDir)
|
|
|
|
// Re-create the structure in the temp dir
|
|
assert.NoError(t, os.MkdirAll(filepath.Join(".via", "devmode"), 0755))
|
|
p2 := filepath.Join(".via", "devmode", "ctx.json")
|
|
f2, _ := os.Create(p2)
|
|
json.NewEncoder(f2).Encode(map[string]string{"test-ctx-1": "/"})
|
|
f2.Close()
|
|
|
|
c := newContext("test-ctx-1", "/", v)
|
|
v.devModeRemovePersisted(c)
|
|
|
|
// Read back and verify
|
|
f3, err := os.Open(p2)
|
|
assert.NoError(t, err)
|
|
defer f3.Close()
|
|
var result map[string]string
|
|
assert.NoError(t, json.NewDecoder(f3).Decode(&result))
|
|
assert.Empty(t, result, "persisted context should be removed")
|
|
}
|