Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions addons/htmx/htmx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package htmx

import (
"encoding/json"

"github.com/buildwithgo/amaro"
)

// Is returns true if the request is an HTMX request.
func Is(c *amaro.Context) bool {
return c.GetHeader("HX-Request") == "true"
}

// Trigger sets the HX-Trigger header to trigger a client-side event.
func Trigger(c *amaro.Context, event string) {
c.SetHeader("HX-Trigger", event)
}

// TriggerJSON sets the HX-Trigger header with a JSON object for passing data to events.
func TriggerJSON(c *amaro.Context, events map[string]any) error {
b, err := json.Marshal(events)
if err != nil {
return err
}
c.SetHeader("HX-Trigger", string(b))
return nil
}

// PushURL sets the HX-Push-Url header to push a new URL into the history stack.
func PushURL(c *amaro.Context, url string) {
c.SetHeader("HX-Push-Url", url)
}

// Redirect sets the HX-Redirect header to force a client-side redirect.
func Redirect(c *amaro.Context, url string) {
c.SetHeader("HX-Redirect", url)
}

// Refresh sets the HX-Refresh header to force a full page refresh.
func Refresh(c *amaro.Context) {
c.SetHeader("HX-Refresh", "true")
}

// Retarget sets the HX-Retarget header to update a different element than the one triggering the request.
func Retarget(c *amaro.Context, target string) {
c.SetHeader("HX-Retarget", target)
}

// Reswap sets the HX-Reswap header to specify how the response should be swapped in.
func Reswap(c *amaro.Context, swap string) {
c.SetHeader("HX-Reswap", swap)
}
86 changes: 86 additions & 0 deletions addons/htmx/htmx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package htmx_test

import (
"encoding/json"
"net/http/httptest"
"testing"

"github.com/buildwithgo/amaro"
"github.com/buildwithgo/amaro/addons/htmx"
)

func TestHTMX(t *testing.T) {
t.Run("Is", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
c := amaro.NewContext(w, req)

if !htmx.Is(c) {
t.Error("Expected IsHTMX to return true")
}
})

t.Run("Trigger", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
c := amaro.NewContext(w, req)

htmx.Trigger(c, "myEvent")
if w.Header().Get("HX-Trigger") != "myEvent" {
t.Errorf("Expected HX-Trigger header 'myEvent', got %s", w.Header().Get("HX-Trigger"))
}
})

t.Run("TriggerJSON", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
c := amaro.NewContext(w, req)

events := map[string]any{
"event1": "data1",
"event2": 123,
}
if err := htmx.TriggerJSON(c, events); err != nil {
t.Fatalf("TriggerJSON failed: %v", err)
}

header := w.Header().Get("HX-Trigger")
var decoded map[string]any
if err := json.Unmarshal([]byte(header), &decoded); err != nil {
t.Fatalf("Failed to unmarshal HX-Trigger header: %v", err)
}

if decoded["event1"] != "data1" {
t.Errorf("Expected event1 data1, got %v", decoded["event1"])
}
})

t.Run("Headers", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
c := amaro.NewContext(w, req)

htmx.PushURL(c, "/new-url")
htmx.Redirect(c, "/redirect")
htmx.Refresh(c)
htmx.Retarget(c, "#target")
htmx.Reswap(c, "outerHTML")

if w.Header().Get("HX-Push-Url") != "/new-url" {
t.Error("PushURL failed")
}
if w.Header().Get("HX-Redirect") != "/redirect" {
t.Error("Redirect failed")
}
if w.Header().Get("HX-Refresh") != "true" {
t.Error("Refresh failed")
}
if w.Header().Get("HX-Retarget") != "#target" {
t.Error("Retarget failed")
}
if w.Header().Get("HX-Reswap") != "outerHTML" {
t.Error("Reswap failed")
}
})
}
9 changes: 9 additions & 0 deletions amaro.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io/fs"
"log"
"net/http"
"net/http/httptest"
"os"
"os/signal"
"strings"
Expand Down Expand Up @@ -100,6 +101,14 @@ func (a *App) Find(method, path string) (*Route, error) {
return a.router.Find(method, path, nil)
}

// Test executes a request against the application and returns the response recorder.
// This is a helper for writing tests.
func (a *App) Test(req *http.Request) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
a.ServeHTTP(w, req)
return w
}

// AppOption defines a function to configure the App during initialization.
type AppOption func(*App)

Expand Down
28 changes: 28 additions & 0 deletions amaro_test_helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package amaro_test

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/buildwithgo/amaro"
"github.com/buildwithgo/amaro/routers"
)

func TestAppTestHelper(t *testing.T) {
app := amaro.New(amaro.WithRouter(routers.NewTrieRouter()))

app.GET("/hello", func(c *amaro.Context) error {
return c.String(http.StatusOK, "world")
})

req := httptest.NewRequest("GET", "/hello", nil)
w := app.Test(req)

if w.Code != http.StatusOK {
t.Errorf("Expected 200 OK, got %d", w.Code)
}
if w.Body.String() != "world" {
t.Errorf("Expected 'world', got %s", w.Body.String())
}
}
Loading