Documentation
¶
Overview ¶
Package datastartest provides helpers for E2E testing DataStar handlers.
It solves the two problems that make DataStar handlers hard to test:
- Parsing the SSE wire format (event:/data:/id:/retry: lines) into events.
- Decoding DataStar datalines (selector/mode/elements/signals key-value pairs) back into typed, assertable values.
The library's own e2e_test.go in the parent package hand-rolls parsing code for the same purpose. This package exports that logic so consumers don't have to reinvent it.
Quick start ¶
import (
"github.com/larsartmann/go-datastar"
"github.com/larsartmann/go-datastar/datastartest"
"github.com/larsartmann/go-sse"
)
func TestFeedHandler(t *testing.T) {
events := datastartest.Collect(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
stream := sse.NewStream(w, r)
defer func() { _ = stream.Close() }()
resp := datastar.NewResponse(stream)
_ = resp.PatchElements("<div>hello</div>", datastar.WithSelector("#feed"))
_ = resp.MarshalAndPatchSignals(map[string]any{"count": 1})
}))
datastartest.RequireEventCount(t, events, 2)
// Elements patch: typed accessors decode the datalines
el := events[0]
if el.Selector() != "#feed" {
t.Errorf("selector: got %q, want %q", el.Selector(), "#feed")
}
if el.Elements() != "<div>hello</div>" {
t.Errorf("elements: got %q", el.Elements())
}
// Signals patch: unmarshal into a struct
var signals struct {
Count int `json:"count"`
}
if err := events[1].UnmarshalSignals(&signals); err != nil {
t.Fatalf("unmarshal signals: %v", err)
}
if signals.Count != 1 {
t.Errorf("count: got %d, want 1", signals.Count)
}
}
Non-GET requests ¶
For POST/PUT/PATCH with request bodies, use CollectPost or CollectWithRequest:
events := datastartest.CollectPost(t, handler, `{"name":"alice"}`)
events := datastartest.CollectWithRequest(t, handler, http.MethodPut, body, "application/json")
Streaming handlers ¶
For handlers that keep the connection open (e.g., broadcasting through a go-sse Broadcaster), use CollectN to read exactly N events then close. Use CollectWithTimeout for a time-bounded read that returns whatever events arrived before the deadline:
events := datastartest.CollectN(t, handler, 3) events := datastartest.CollectWithTimeout(t, handler, 5*time.Second)
Script patches ¶
Script patches (ExecuteScript, Redirect, ConsoleLog, etc.) produce patch-elements events with JS wrapped in <script> tags. Use Event.IsScript to check and Event.ScriptContent to extract the inner JavaScript source:
events := datastartest.Collect(t, handler)
if events[0].IsScript() {
js := events[0].ScriptContent() // "console.log('hello')"
}
Finding events ¶
When a handler sends multiple patches, use FindElement and FindSignals to locate specific events by selector or type without indexing by position:
evt, ok := datastartest.FindElement(events, "#header") sigEvt, ok := datastartest.FindSignals(events)
Debugging ¶
Use Event.String and EventsString for human-readable representations useful in test failure messages:
t.Fatalf("unexpected events:\n%s", datastartest.EventsString(events))
Index ¶
- Constants
- func EventsString(events []Event) string
- func RequireElements(t *testing.T, evt Event, wantSelector, wantMode, wantHTML string)
- func RequireElementsContains(t *testing.T, evt Event, wantSelector, wantMode, wantHTMLContains string)
- func RequireEventCount(t *testing.T, events []Event, want int)
- func RequireEventType(t *testing.T, evt Event, want string)
- func RequireSignals(t *testing.T, evt Event, wantJSON string)
- func RequireSignalsContain(t *testing.T, evt Event, key string)
- type Event
- func Collect(t *testing.T, handler http.Handler) []Event
- func CollectN(t *testing.T, handler http.Handler, count int) []Event
- func CollectPost(t *testing.T, handler http.Handler, jsonBody string) []Event
- func CollectWithRequest(t *testing.T, handler http.Handler, method string, body io.Reader, ...) []Event
- func CollectWithTimeout(t *testing.T, handler http.Handler, timeout time.Duration) []Event
- func FilterElements(events []Event) []Event
- func FilterSignals(events []Event) []Event
- func FindElement(events []Event, selector string) (Event, bool)
- func FindSignals(events []Event) (Event, bool)
- func MustReadEvents(t *testing.T, r io.Reader) []Event
- func MustReadNEvents(t *testing.T, r io.Reader, count int) []Event
- func ReadEvents(r io.Reader) ([]Event, error)
- func ReadNEvents(r io.Reader, count int) ([]Event, error)
- func (e Event) DataValue(key string) string
- func (e Event) Elements() string
- func (e Event) IsElements() bool
- func (e Event) IsScript() bool
- func (e Event) IsSignals() bool
- func (e Event) Mode() string
- func (e Event) Namespace() string
- func (e Event) OnlyIfMissing() bool
- func (e Event) ScriptContent() string
- func (e Event) Selector() string
- func (e Event) SignalsJSON() []byte
- func (e Event) String() string
- func (e Event) UnmarshalSignals(target any) error
- func (e Event) UseViewTransitions() bool
- func (e Event) ViewTransitionSelector() string
Examples ¶
Constants ¶
const ( // CodeSSEScanFailed: [ReadEvents] or [ReadNEvents] encountered an I/O // error while scanning the SSE response stream. CodeSSEScanFailed = "datastartest.sse_scan_failed" // CodeSignalsUnmarshalFailed: [Event.UnmarshalSignals] could not decode // the signals JSON payload from a patch-signals event. CodeSignalsUnmarshalFailed = "datastartest.signals_unmarshal_failed" )
Error codes for datastartest. Each is a stable string accessible via errorfamily.Code, enabling programmatic classification of errors returned by the test helpers without string matching on human-readable messages.
Variables ¶
This section is empty.
Functions ¶
func EventsString ¶
EventsString returns a multi-line debug representation of an event slice, with one Event per line. Useful for logging test failures involving multiple events.
Example ¶
ExampleEventsString demonstrates the multi-event debug representation. Useful for logging when a test assertion fails on a specific event.
package main
import (
"fmt"
"strings"
"github.com/larsartmann/go-datastar/datastartest"
)
func main() {
sseOutput := "event: datastar-patch-elements\ndata: elements <div>1</div>\n\n" +
"event: datastar-patch-signals\ndata: signals {\"x\":1}\n\n"
events, _ := datastartest.ReadEvents(strings.NewReader(sseOutput))
fmt.Println(datastartest.EventsString(events))
}
Output: Event{type=datastar-patch-elements datalines=1} Event{type=datastar-patch-signals datalines=1}
func RequireElements ¶
RequireElements fails the test unless evt is a patch-elements event with the exact selector, mode, and HTML content. Use RequireElementsContains when you need a substring match on the HTML (e.g., for script patches).
Example ¶
ExampleRequireElements demonstrates the assertion helpers.
package main
import (
"fmt"
)
func main() {
// In your test:
//
// events := datastartest.Collect(t, handler)
// datastartest.RequireEventCount(t, events, 1)
// datastartest.RequireElements(t, events[0], "#feed", "append", "<div>hello</div>")
// datastartest.RequireElementsContains(t, events[0], "body", "append", "console.log")
//
// These helpers produce clear failure messages showing exactly what mismatched.
fmt.Println("Assert helpers: RequireElements, RequireElementsContains, RequireSignals")
}
Output: Assert helpers: RequireElements, RequireElementsContains, RequireSignals
func RequireElementsContains ¶
func RequireElementsContains( t *testing.T, evt Event, wantSelector, wantMode, wantHTMLContains string, )
RequireElementsContains fails the test unless evt is a patch-elements event with the exact selector and mode, and Elements() contains wantHTMLContains as a substring. Useful for script patches where the HTML includes wrapper elements (e.g., <script>) around the content you want to verify.
func RequireEventCount ¶
RequireEventCount fails the test unless events has exactly want events.
func RequireEventType ¶
RequireEventType fails the test unless the event type matches want.
func RequireSignals ¶
RequireSignals fails the test unless evt is a patch-signals event whose JSON payload equals wantJSON exactly.
func RequireSignalsContain ¶
RequireSignalsContain fails the test unless evt is a patch-signals event whose JSON payload contains key at any nesting level. This is a convenience for checking individual signal keys without decoding the full payload.
Types ¶
type Event ¶
Event is a DataStar SSE event decoded from the wire format. It preserves the raw data lines and provides typed accessors that decode the DataStar dataline key-value pairs.
Fields:
- Type is the SSE event type (e.g., "datastar-patch-elements").
- DataLines are the individual data: lines with their key prefixes intact.
- ID is the optional SSE event ID.
- Retry is the optional reconnection interval in milliseconds.
Example (ScriptContent) ¶
ExampleEvent_scriptContent demonstrates extracting JavaScript from a script patch. Script patches (ExecuteScript, Redirect, ConsoleLog, etc.) wrap JS in <script> tags inside a patch-elements event. ScriptContent strips the wrapper and returns the JS.
package main
import (
"fmt"
"strings"
"github.com/larsartmann/go-datastar/datastartest"
)
func main() {
sseOutput := "event: datastar-patch-elements\n" +
"data: selector body\n" +
"data: mode append\n" +
"data: elements <script>console.log('hello')</script>\n\n"
events, _ := datastartest.ReadEvents(strings.NewReader(sseOutput))
fmt.Println(events[0].IsScript())
fmt.Println(events[0].ScriptContent())
}
Output: true console.log('hello')
Example (UnmarshalSignals) ¶
ExampleEvent_unmarshalSignals demonstrates decoding signals JSON into a struct.
package main
import (
"fmt"
"strings"
"github.com/larsartmann/go-datastar/datastartest"
)
func main() {
sseOutput := "event: datastar-patch-signals\n" +
`data: signals {"count":42,"name":"alice"}` + "\n\n"
events, _ := datastartest.ReadEvents(strings.NewReader(sseOutput))
var data struct {
Count int `json:"count"`
Name string `json:"name"`
}
_ = events[0].UnmarshalSignals(&data)
fmt.Printf("count=%d name=%s", data.Count, data.Name)
}
Output: count=42 name=alice
func Collect ¶
Collect starts a test server for the handler, sends a GET request, reads the full SSE response, and returns decoded DataStar events.
This is the simplest way to E2E test a synchronous DataStar handler. The handler should send all patches and return (closing the stream).
For non-GET requests, custom headers, or request bodies, use CollectWithRequest or CollectPost.
For streaming handlers that keep the connection open, use CollectN.
Example ¶
ExampleCollect demonstrates the simplest way to E2E test a DataStar handler.
datastartest.Collect spins up a test server, sends a GET request, parses the SSE response, and returns decoded events with typed accessors.
package main
import (
"fmt"
"net/http"
"strings"
"github.com/larsartmann/go-datastar"
"github.com/larsartmann/go-datastar/datastartest"
"github.com/larsartmann/go-sse"
)
func main() {
// Your production handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
stream := sse.NewStream(w, r)
defer func() { _ = stream.Close() }()
resp := datastar.NewResponse(stream)
_ = resp.PatchElements("<div>hello</div>", datastar.WithSelector("#feed"))
})
// In your test: events := datastartest.Collect(t, handler)
//
// Here we parse the same wire format manually to demonstrate the decoded shape.
sseOutput := "event: datastar-patch-elements\n" +
"data: selector #feed\n" +
"data: elements <div>hello</div>\n\n"
events, _ := datastartest.ReadEvents(strings.NewReader(sseOutput))
fmt.Println("type:", events[0].Type)
fmt.Println("selector:", events[0].Selector())
fmt.Println("elements:", events[0].Elements())
_ = handler
}
Output: type: datastar-patch-elements selector: #feed elements: <div>hello</div>
func CollectN ¶
CollectN starts a test server, sends a GET request, and reads exactly n events from the SSE stream before closing the connection. Use this for streaming handlers that keep the connection open (e.g., broadcasting through a Broadcaster). Unlike Collect, this does not wait for the handler to finish — it returns as soon as n events have been received.
func CollectPost ¶
CollectPost is a convenience wrapper around CollectWithRequest for POST requests with a JSON body — the most common non-GET pattern for DataStar handlers (e.g., submitting a form that updates signals).
func CollectWithRequest ¶
func CollectWithRequest( t *testing.T, handler http.Handler, method string, body io.Reader, contentType string, ) []Event
CollectWithRequest starts a test server, sends a request with the given method, body, and content type, reads the full SSE response, and returns decoded DataStar events. Use this for POST/PUT/PATCH handlers that expect request bodies.
For the common POST-JSON case, prefer CollectPost.
func CollectWithTimeout ¶
CollectWithTimeout is like Collect but enforces a maximum duration. If the handler does not close the stream within timeout, the context cancels and whatever events were received so far are returned. If no events were received before the timeout, the test fails.
Use this for defensive testing against handlers that might hang.
func FilterElements ¶
FilterElements returns only the patch-elements events from the slice.
Example ¶
ExampleFilterElements demonstrates filtering events by type.
package main
import (
"fmt"
"strings"
"github.com/larsartmann/go-datastar/datastartest"
)
func main() {
sseOutput := "event: datastar-patch-elements\ndata: elements <div>1</div>\n\n" +
"event: datastar-patch-signals\ndata: signals {\"x\":1}\n\n" +
"event: datastar-patch-elements\ndata: elements <div>2</div>\n\n"
events, _ := datastartest.ReadEvents(strings.NewReader(sseOutput))
elements := datastartest.FilterElements(events)
signals := datastartest.FilterSignals(events)
fmt.Printf("%d elements, %d signals", len(elements), len(signals))
}
Output: 2 elements, 1 signals
func FilterSignals ¶
FilterSignals returns only the patch-signals events from the slice.
func FindElement ¶
FindElement returns the first patch-elements event whose CSS selector matches the given value, along with true. Returns false if no match is found.
Useful when a handler sends multiple elements patches and you need to assert on a specific one without indexing by position.
Example ¶
ExampleFindElement demonstrates finding a specific elements patch by selector when a handler sends multiple patches.
package main
import (
"fmt"
"strings"
"github.com/larsartmann/go-datastar/datastartest"
)
func main() {
sseOutput := "event: datastar-patch-elements\ndata: selector #header\ndata: elements <h1>Title</h1>\n\n" +
"event: datastar-patch-signals\ndata: signals {\"count\":1}\n\n" +
"event: datastar-patch-elements\ndata: selector #body\ndata: elements <p>Content</p>\n\n"
events, _ := datastartest.ReadEvents(strings.NewReader(sseOutput))
evt, ok := datastartest.FindElement(events, "#body")
fmt.Printf("found=%v elements=%s", ok, evt.Elements())
}
Output: found=true elements=<p>Content</p>
func FindSignals ¶
FindSignals returns the first patch-signals event, along with true. Returns false if the slice contains no signals events.
Example ¶
ExampleFindSignals demonstrates finding the first signals event in a stream.
package main
import (
"fmt"
"strings"
"github.com/larsartmann/go-datastar/datastartest"
)
func main() {
sseOutput := "event: datastar-patch-elements\ndata: elements <div>1</div>\n\n" +
"event: datastar-patch-signals\ndata: signals {\"step\":2}\n\n"
events, _ := datastartest.ReadEvents(strings.NewReader(sseOutput))
evt, ok := datastartest.FindSignals(events)
fmt.Printf("found=%v type=%s", ok, evt.Type)
}
Output: found=true type=datastar-patch-signals
func MustReadEvents ¶
MustReadEvents is like ReadEvents but calls t.Fatal on error.
func MustReadNEvents ¶
MustReadNEvents is like ReadNEvents but calls t.Fatal on error. Use this with streaming SSE connections that do not close on their own.
func ReadEvents ¶
ReadEvents parses the SSE wire format from r and returns all decoded events. It reads until EOF, so the source must close or end the stream (e.g., an HTTP response body from a handler that sends all patches and returns).
The parser handles the standard SSE fields: event, data, id, retry, and comment lines (starting with ":"). Each blank line dispatches the current event. An event without a trailing blank line at EOF is still returned.
DataStar datalines are preserved individually in Event.DataLines with their key prefixes intact (e.g., "selector #feed"), so typed accessors like Event.Selector and Event.Elements can decode them.
func ReadNEvents ¶
ReadNEvents reads up to n events from r. Returns as soon as n events have been dispatched, without waiting for EOF. This is the streaming-reader counterpart to ReadEvents: use it with a live SSE connection body that does not close on its own (e.g., a handler broadcasting through a Broadcaster).
A scanner error after events have been collected is treated as a clean connection close, not a failure.
func (Event) DataValue ¶
DataValue returns the value after the first dataline matching the given key prefix (e.g., "selector ", "mode "). This is a generic escape hatch when no typed accessor covers a specific dataline key. Returns empty if not found.
func (Event) Elements ¶
Elements returns the HTML content from a patch-elements event. Multi-line HTML that was split across multiple "elements" datalines is rejoined with "\n", reconstructing the original content.
func (Event) IsElements ¶
IsElements reports whether the event type is datastar-patch-elements.
func (Event) IsScript ¶
IsScript reports whether the event is a patch-elements event whose HTML content starts with a <script> tag. Script patches (ExecuteScript, Redirect, ConsoleLog, ConsoleError, DispatchCustomEvent, ReplaceURL, Prefetch) all produce elements patches wrapping JavaScript in <script> tags.
func (Event) Mode ¶
Mode returns the element patch mode (e.g., "append", "inner", "remove"). Returns "outer" (the DataStar default) if no mode dataline is present.
func (Event) Namespace ¶
Namespace returns the XML namespace for a patch-elements event. Returns "html" (the DataStar default) if no namespace dataline is present.
func (Event) OnlyIfMissing ¶
OnlyIfMissing reports whether the signals patch has the onlyIfMissing flag.
func (Event) ScriptContent ¶
ScriptContent extracts the JavaScript source from a script-bearing patch. Script patches (ExecuteScript, Redirect, ConsoleLog, ConsoleError, DispatchCustomEvent, ReplaceURL, Prefetch) wrap JS inside <script> tags within a patch-elements event. This method strips the <script ...> wrapper and returns the inner source code.
Returns empty string if the event is not a script-bearing elements patch.
func (Event) Selector ¶
Selector returns the CSS selector from a patch-elements event. Returns empty if not present (the client defaults to the merging element).
func (Event) SignalsJSON ¶
SignalsJSON returns the raw JSON bytes from a patch-signals event. Multi-line JSON that was split across multiple "signals" datalines is rejoined with "\n", reconstructing the original payload.
func (Event) String ¶
String returns a human-readable debug representation of the event, showing the type, event ID (if any), retry (if non-zero), and dataline count. Useful for debugging test failures and logging.
func (Event) UnmarshalSignals ¶
UnmarshalSignals decodes the signals JSON payload from a patch-signals event into the target. The target must be a pointer.
func (Event) UseViewTransitions ¶
UseViewTransitions reports whether the event enables the View Transition API.
func (Event) ViewTransitionSelector ¶
ViewTransitionSelector returns the scoped view transition selector, if any.