Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d61149fa3 | ||
|
|
08b7dbd17f | ||
|
|
cd2bfb6978 |
14
.claude/commands/release.md
Normal file
14
.claude/commands/release.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Create a new release for this project. Steps:
|
||||||
|
|
||||||
|
1. Fetch tags from all remotes so the version list is current.
|
||||||
|
2. Check for uncommitted changes. If any exist, commit them with a clean semantic commit message. No Claude attribution lines.
|
||||||
|
3. Review the commits since the last tag. Based on their content, recommend a semver bump:
|
||||||
|
- **major**: breaking/incompatible API changes
|
||||||
|
- **minor**: new features, meaningful new behavior
|
||||||
|
- **patch**: bug fixes, docs, refactoring with no new features
|
||||||
|
Present the proposed version, the bump rationale, and the commit list. Wait for user approval before continuing.
|
||||||
|
4. Tag the new version and push the tag + commits to all remotes (origin, gitea, etc.).
|
||||||
|
5. Generate release notes from the commits since the last tag, grouped by type (features, fixes, docs/refactoring).
|
||||||
|
6. Create a GitHub release using `gh release create`.
|
||||||
|
7. Create a Gitea release using `tea releases create` with the same notes.
|
||||||
|
8. Report both release URLs and confirm all remotes are up to date.
|
||||||
@@ -3,6 +3,7 @@ package via
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ryanhamamura/via/h"
|
"github.com/ryanhamamura/via/h"
|
||||||
)
|
)
|
||||||
@@ -23,6 +24,8 @@ type triggerOpts struct {
|
|||||||
value string
|
value string
|
||||||
window bool
|
window bool
|
||||||
preventDefault bool
|
preventDefault bool
|
||||||
|
debounce time.Duration
|
||||||
|
throttle time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type withSignalOpt struct {
|
type withSignalOpt struct {
|
||||||
@@ -58,6 +61,41 @@ func WithPreventDefault() ActionTriggerOption {
|
|||||||
return withPreventDefaultOpt{}
|
return withPreventDefaultOpt{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type withDebounceOpt struct{ d time.Duration }
|
||||||
|
|
||||||
|
func (o withDebounceOpt) apply(opts *triggerOpts) { opts.debounce = o.d }
|
||||||
|
|
||||||
|
// WithDebounce adds a debounce modifier to the event trigger.
|
||||||
|
func WithDebounce(d time.Duration) ActionTriggerOption { return withDebounceOpt{d} }
|
||||||
|
|
||||||
|
type withThrottleOpt struct{ d time.Duration }
|
||||||
|
|
||||||
|
func (o withThrottleOpt) apply(opts *triggerOpts) { opts.throttle = o.d }
|
||||||
|
|
||||||
|
// WithThrottle adds a throttle modifier to the event trigger.
|
||||||
|
func WithThrottle(d time.Duration) ActionTriggerOption { return withThrottleOpt{d} }
|
||||||
|
|
||||||
|
// formatDuration renders a duration as e.g. "200ms" for Datastar modifiers.
|
||||||
|
func formatDuration(d time.Duration) string {
|
||||||
|
return fmt.Sprintf("%dms", d.Milliseconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildAttrKey constructs a Datastar attribute key with modifiers.
|
||||||
|
// Order: event → debounce/throttle → window.
|
||||||
|
func buildAttrKey(event string, opts *triggerOpts) string {
|
||||||
|
key := "on:" + event
|
||||||
|
if opts.debounce > 0 {
|
||||||
|
key += "__debounce." + formatDuration(opts.debounce)
|
||||||
|
}
|
||||||
|
if opts.throttle > 0 {
|
||||||
|
key += "__throttle." + formatDuration(opts.throttle)
|
||||||
|
}
|
||||||
|
if opts.window {
|
||||||
|
key += "__window"
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
// WithSignal sets a signal value before triggering the action.
|
// WithSignal sets a signal value before triggering the action.
|
||||||
func WithSignal(sig *signal, value string) ActionTriggerOption {
|
func WithSignal(sig *signal, value string) ActionTriggerOption {
|
||||||
return withSignalOpt{
|
return withSignalOpt{
|
||||||
@@ -97,62 +135,62 @@ func actionURL(id string) string {
|
|||||||
// to element nodes in a view.
|
// to element nodes in a view.
|
||||||
func (a *actionTrigger) OnClick(options ...ActionTriggerOption) h.H {
|
func (a *actionTrigger) OnClick(options ...ActionTriggerOption) h.H {
|
||||||
opts := applyOptions(options...)
|
opts := applyOptions(options...)
|
||||||
return h.Data("on:click", buildOnExpr(actionURL(a.id), &opts))
|
return h.Data(buildAttrKey("click", &opts), buildOnExpr(actionURL(a.id), &opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnChange returns a via.h DOM attribute that triggers on input change. It can be added
|
// OnChange returns a via.h DOM attribute that triggers on input change. It can be added
|
||||||
// to element nodes in a view.
|
// to element nodes in a view.
|
||||||
func (a *actionTrigger) OnChange(options ...ActionTriggerOption) h.H {
|
func (a *actionTrigger) OnChange(options ...ActionTriggerOption) h.H {
|
||||||
opts := applyOptions(options...)
|
opts := applyOptions(options...)
|
||||||
return h.Data("on:change__debounce.200ms", buildOnExpr(actionURL(a.id), &opts))
|
return h.Data(buildAttrKey("change", &opts), buildOnExpr(actionURL(a.id), &opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnSubmit returns a via.h DOM attribute that triggers on form submit.
|
// OnSubmit returns a via.h DOM attribute that triggers on form submit.
|
||||||
func (a *actionTrigger) OnSubmit(options ...ActionTriggerOption) h.H {
|
func (a *actionTrigger) OnSubmit(options ...ActionTriggerOption) h.H {
|
||||||
opts := applyOptions(options...)
|
opts := applyOptions(options...)
|
||||||
return h.Data("on:submit", buildOnExpr(actionURL(a.id), &opts))
|
return h.Data(buildAttrKey("submit", &opts), buildOnExpr(actionURL(a.id), &opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnInput returns a via.h DOM attribute that triggers on input (without debounce).
|
// OnInput returns a via.h DOM attribute that triggers on input (without debounce).
|
||||||
func (a *actionTrigger) OnInput(options ...ActionTriggerOption) h.H {
|
func (a *actionTrigger) OnInput(options ...ActionTriggerOption) h.H {
|
||||||
opts := applyOptions(options...)
|
opts := applyOptions(options...)
|
||||||
return h.Data("on:input", buildOnExpr(actionURL(a.id), &opts))
|
return h.Data(buildAttrKey("input", &opts), buildOnExpr(actionURL(a.id), &opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnFocus returns a via.h DOM attribute that triggers when the element gains focus.
|
// OnFocus returns a via.h DOM attribute that triggers when the element gains focus.
|
||||||
func (a *actionTrigger) OnFocus(options ...ActionTriggerOption) h.H {
|
func (a *actionTrigger) OnFocus(options ...ActionTriggerOption) h.H {
|
||||||
opts := applyOptions(options...)
|
opts := applyOptions(options...)
|
||||||
return h.Data("on:focus", buildOnExpr(actionURL(a.id), &opts))
|
return h.Data(buildAttrKey("focus", &opts), buildOnExpr(actionURL(a.id), &opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnBlur returns a via.h DOM attribute that triggers when the element loses focus.
|
// OnBlur returns a via.h DOM attribute that triggers when the element loses focus.
|
||||||
func (a *actionTrigger) OnBlur(options ...ActionTriggerOption) h.H {
|
func (a *actionTrigger) OnBlur(options ...ActionTriggerOption) h.H {
|
||||||
opts := applyOptions(options...)
|
opts := applyOptions(options...)
|
||||||
return h.Data("on:blur", buildOnExpr(actionURL(a.id), &opts))
|
return h.Data(buildAttrKey("blur", &opts), buildOnExpr(actionURL(a.id), &opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnMouseEnter returns a via.h DOM attribute that triggers when the mouse enters the element.
|
// OnMouseEnter returns a via.h DOM attribute that triggers when the mouse enters the element.
|
||||||
func (a *actionTrigger) OnMouseEnter(options ...ActionTriggerOption) h.H {
|
func (a *actionTrigger) OnMouseEnter(options ...ActionTriggerOption) h.H {
|
||||||
opts := applyOptions(options...)
|
opts := applyOptions(options...)
|
||||||
return h.Data("on:mouseenter", buildOnExpr(actionURL(a.id), &opts))
|
return h.Data(buildAttrKey("mouseenter", &opts), buildOnExpr(actionURL(a.id), &opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnMouseLeave returns a via.h DOM attribute that triggers when the mouse leaves the element.
|
// OnMouseLeave returns a via.h DOM attribute that triggers when the mouse leaves the element.
|
||||||
func (a *actionTrigger) OnMouseLeave(options ...ActionTriggerOption) h.H {
|
func (a *actionTrigger) OnMouseLeave(options ...ActionTriggerOption) h.H {
|
||||||
opts := applyOptions(options...)
|
opts := applyOptions(options...)
|
||||||
return h.Data("on:mouseleave", buildOnExpr(actionURL(a.id), &opts))
|
return h.Data(buildAttrKey("mouseleave", &opts), buildOnExpr(actionURL(a.id), &opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnScroll returns a via.h DOM attribute that triggers on scroll.
|
// OnScroll returns a via.h DOM attribute that triggers on scroll.
|
||||||
func (a *actionTrigger) OnScroll(options ...ActionTriggerOption) h.H {
|
func (a *actionTrigger) OnScroll(options ...ActionTriggerOption) h.H {
|
||||||
opts := applyOptions(options...)
|
opts := applyOptions(options...)
|
||||||
return h.Data("on:scroll", buildOnExpr(actionURL(a.id), &opts))
|
return h.Data(buildAttrKey("scroll", &opts), buildOnExpr(actionURL(a.id), &opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnDblClick returns a via.h DOM attribute that triggers on double click.
|
// OnDblClick returns a via.h DOM attribute that triggers on double click.
|
||||||
func (a *actionTrigger) OnDblClick(options ...ActionTriggerOption) h.H {
|
func (a *actionTrigger) OnDblClick(options ...ActionTriggerOption) h.H {
|
||||||
opts := applyOptions(options...)
|
opts := applyOptions(options...)
|
||||||
return h.Data("on:dblclick", buildOnExpr(actionURL(a.id), &opts))
|
return h.Data(buildAttrKey("dblclick", &opts), buildOnExpr(actionURL(a.id), &opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnKeyDown returns a via.h DOM attribute that triggers when a key is pressed.
|
// OnKeyDown returns a via.h DOM attribute that triggers when a key is pressed.
|
||||||
@@ -164,11 +202,7 @@ func (a *actionTrigger) OnKeyDown(key string, options ...ActionTriggerOption) h.
|
|||||||
if key != "" {
|
if key != "" {
|
||||||
condition = fmt.Sprintf("evt.key==='%s' &&", key)
|
condition = fmt.Sprintf("evt.key==='%s' &&", key)
|
||||||
}
|
}
|
||||||
attrName := "on:keydown"
|
return h.Data(buildAttrKey("keydown", &opts), fmt.Sprintf("%s%s", condition, buildOnExpr(actionURL(a.id), &opts)))
|
||||||
if opts.window {
|
|
||||||
attrName = "on:keydown__window"
|
|
||||||
}
|
|
||||||
return h.Data(attrName, fmt.Sprintf("%s%s", condition, buildOnExpr(actionURL(a.id), &opts)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyBinding pairs a key with an action and per-binding options.
|
// KeyBinding pairs a key with an action and per-binding options.
|
||||||
|
|||||||
12
nats.go
12
nats.go
@@ -56,7 +56,19 @@ func startDefaultNATS() (dn *defaultNATS, err error) {
|
|||||||
os.RemoveAll(dataDir)
|
os.RemoveAll(dataDir)
|
||||||
return nil, fmt.Errorf("start embedded nats: %w", err)
|
return nil, fmt.Errorf("start embedded nats: %w", err)
|
||||||
}
|
}
|
||||||
|
ready := make(chan struct{})
|
||||||
|
go func() {
|
||||||
ns.WaitForServer()
|
ns.WaitForServer()
|
||||||
|
close(ready)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-ready:
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
ns.Close()
|
||||||
|
cancel()
|
||||||
|
os.RemoveAll(dataDir)
|
||||||
|
return nil, fmt.Errorf("embedded nats server did not start within 10s")
|
||||||
|
}
|
||||||
|
|
||||||
nc, err := ns.Client()
|
nc, err := ns.Client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
31
nats_test.go
31
nats_test.go
@@ -10,9 +10,24 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPubSub_RoundTrip(t *testing.T) {
|
// setupNATSTest creates a *V with an embedded NATS server.
|
||||||
|
// Skips the test if NATS fails to start (e.g. port conflict in CI).
|
||||||
|
func setupNATSTest(t *testing.T) *V {
|
||||||
|
t.Helper()
|
||||||
v := New()
|
v := New()
|
||||||
defer v.Shutdown()
|
dn, err := getSharedNATS()
|
||||||
|
if err != nil {
|
||||||
|
v.Shutdown()
|
||||||
|
t.Skipf("embedded NATS unavailable: %v", err)
|
||||||
|
}
|
||||||
|
v.defaultNATS = dn
|
||||||
|
v.pubsub = &natsRef{dn: dn}
|
||||||
|
t.Cleanup(v.Shutdown)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPubSub_RoundTrip(t *testing.T) {
|
||||||
|
v := setupNATSTest(t)
|
||||||
|
|
||||||
var received []byte
|
var received []byte
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
@@ -38,8 +53,7 @@ func TestPubSub_RoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPubSub_MultipleSubscribers(t *testing.T) {
|
func TestPubSub_MultipleSubscribers(t *testing.T) {
|
||||||
v := New()
|
v := setupNATSTest(t)
|
||||||
defer v.Shutdown()
|
|
||||||
|
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
var results []string
|
var results []string
|
||||||
@@ -84,8 +98,7 @@ func TestPubSub_MultipleSubscribers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPubSub_SubscriptionCleanupOnDispose(t *testing.T) {
|
func TestPubSub_SubscriptionCleanupOnDispose(t *testing.T) {
|
||||||
v := New()
|
v := setupNATSTest(t)
|
||||||
defer v.Shutdown()
|
|
||||||
|
|
||||||
c := newContext("cleanup-ctx", "/", v)
|
c := newContext("cleanup-ctx", "/", v)
|
||||||
c.View(func() h.H { return h.Div() })
|
c.View(func() h.H { return h.Div() })
|
||||||
@@ -100,8 +113,7 @@ func TestPubSub_SubscriptionCleanupOnDispose(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPubSub_ManualUnsubscribe(t *testing.T) {
|
func TestPubSub_ManualUnsubscribe(t *testing.T) {
|
||||||
v := New()
|
v := setupNATSTest(t)
|
||||||
defer v.Shutdown()
|
|
||||||
|
|
||||||
c := newContext("unsub-ctx", "/", v)
|
c := newContext("unsub-ctx", "/", v)
|
||||||
c.View(func() h.H { return h.Div() })
|
c.View(func() h.H { return h.Div() })
|
||||||
@@ -120,8 +132,7 @@ func TestPubSub_ManualUnsubscribe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPubSub_NoOpDuringPanicCheck(t *testing.T) {
|
func TestPubSub_NoOpDuringPanicCheck(t *testing.T) {
|
||||||
v := New()
|
v := setupNATSTest(t)
|
||||||
defer v.Shutdown()
|
|
||||||
|
|
||||||
// Panic-check context has id=""
|
// Panic-check context has id=""
|
||||||
c := newContext("", "/", v)
|
c := newContext("", "/", v)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package via
|
package via
|
||||||
|
|
||||||
// PubSub is an interface for publish/subscribe messaging backends.
|
// PubSub is an interface for publish/subscribe messaging backends.
|
||||||
// By default, New() starts an embedded NATS server. Supply a custom
|
// By default, Start() launches an embedded NATS server if no backend
|
||||||
// implementation via Config(Options{PubSub: yourBackend}) to override.
|
// has been configured. Supply a custom implementation via
|
||||||
|
// Config(Options{PubSub: yourBackend}) to override.
|
||||||
type PubSub interface {
|
type PubSub interface {
|
||||||
Publish(subject string, data []byte) error
|
Publish(subject string, data []byte) error
|
||||||
Subscribe(subject string, handler func(data []byte)) (Subscription, error)
|
Subscribe(subject string, handler func(data []byte)) (Subscription, error)
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestPublishSubscribe_RoundTrip(t *testing.T) {
|
func TestPublishSubscribe_RoundTrip(t *testing.T) {
|
||||||
v := New()
|
v := setupNATSTest(t)
|
||||||
defer v.Shutdown()
|
|
||||||
|
|
||||||
type event struct {
|
type event struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -43,8 +42,7 @@ func TestPublishSubscribe_RoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSubscribe_SkipsBadJSON(t *testing.T) {
|
func TestSubscribe_SkipsBadJSON(t *testing.T) {
|
||||||
v := New()
|
v := setupNATSTest(t)
|
||||||
defer v.Shutdown()
|
|
||||||
|
|
||||||
type msg struct {
|
type msg struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
|
|||||||
18
via.go
18
via.go
@@ -358,6 +358,16 @@ func (v *V) reapOrphanedContexts(suspendAfter, ttl time.Duration) {
|
|||||||
// Start starts the Via HTTP server and blocks until a SIGINT or SIGTERM
|
// Start starts the Via HTTP server and blocks until a SIGINT or SIGTERM
|
||||||
// signal is received, then performs a graceful shutdown.
|
// signal is received, then performs a graceful shutdown.
|
||||||
func (v *V) Start() {
|
func (v *V) Start() {
|
||||||
|
if v.pubsub == nil {
|
||||||
|
dn, err := getSharedNATS()
|
||||||
|
if err != nil {
|
||||||
|
v.logWarn(nil, "embedded NATS unavailable: %v", err)
|
||||||
|
} else {
|
||||||
|
v.defaultNATS = dn
|
||||||
|
v.pubsub = &natsRef{dn: dn}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handler := http.Handler(v.mux)
|
handler := http.Handler(v.mux)
|
||||||
if v.sessionManager != nil {
|
if v.sessionManager != nil {
|
||||||
handler = v.sessionManager.LoadAndSave(v.mux)
|
handler = v.sessionManager.LoadAndSave(v.mux)
|
||||||
@@ -833,14 +843,6 @@ func New() *V {
|
|||||||
v.cleanupCtx(c)
|
v.cleanupCtx(c)
|
||||||
})
|
})
|
||||||
|
|
||||||
dn, err := getSharedNATS()
|
|
||||||
if err != nil {
|
|
||||||
v.logWarn(nil, "embedded NATS unavailable: %v", err)
|
|
||||||
} else {
|
|
||||||
v.defaultNATS = dn
|
|
||||||
v.pubsub = &natsRef{dn: dn}
|
|
||||||
}
|
|
||||||
|
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
108
via_test.go
108
via_test.go
@@ -127,7 +127,7 @@ func TestAction(t *testing.T) {
|
|||||||
v.mux.ServeHTTP(w, req)
|
v.mux.ServeHTTP(w, req)
|
||||||
body := w.Body.String()
|
body := w.Body.String()
|
||||||
assert.Contains(t, body, "data-on:click")
|
assert.Contains(t, body, "data-on:click")
|
||||||
assert.Contains(t, body, "data-on:change__debounce.200ms")
|
assert.Contains(t, body, "data-on:change")
|
||||||
assert.Contains(t, body, "data-on:keydown")
|
assert.Contains(t, body, "data-on:keydown")
|
||||||
assert.Contains(t, body, "/_action/")
|
assert.Contains(t, body, "/_action/")
|
||||||
}
|
}
|
||||||
@@ -281,6 +281,112 @@ func TestOnKeyDownMap(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestConfig(t *testing.T) {
|
||||||
v := New()
|
v := New()
|
||||||
v.Config(Options{DocumentTitle: "Test"})
|
v.Config(Options{DocumentTitle: "Test"})
|
||||||
|
|||||||
Reference in New Issue
Block a user