datastartest

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package datastartest provides helpers for E2E testing DataStar handlers.

It solves the two problems that make DataStar handlers hard to test:

  1. Parsing the SSE wire format (event:/data:/id:/retry: lines) into events.
  2. 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

Examples

Constants

View Source
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

func EventsString(events []Event) string

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

func RequireElements(t *testing.T, evt Event, wantSelector, wantMode, wantHTML string)

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

func RequireEventCount(t *testing.T, events []Event, want int)

RequireEventCount fails the test unless events has exactly want events.

func RequireEventType

func RequireEventType(t *testing.T, evt Event, want string)

RequireEventType fails the test unless the event type matches want.

func RequireSignals

func RequireSignals(t *testing.T, evt Event, wantJSON string)

RequireSignals fails the test unless evt is a patch-signals event whose JSON payload equals wantJSON exactly.

func RequireSignalsContain

func RequireSignalsContain(t *testing.T, evt Event, key string)

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

type Event struct {
	Type      string
	DataLines []string
	ID        string
	Retry     uint
}

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

func Collect(t *testing.T, handler http.Handler) []Event

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

func CollectN(t *testing.T, handler http.Handler, count int) []Event

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

func CollectPost(t *testing.T, handler http.Handler, jsonBody string) []Event

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

func CollectWithTimeout(t *testing.T, handler http.Handler, timeout time.Duration) []Event

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

func FilterElements(events []Event) []Event

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

func FilterSignals(events []Event) []Event

FilterSignals returns only the patch-signals events from the slice.

func FindElement

func FindElement(events []Event, selector string) (Event, bool)

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

func FindSignals(events []Event) (Event, bool)

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

func MustReadEvents(t *testing.T, r io.Reader) []Event

MustReadEvents is like ReadEvents but calls t.Fatal on error.

func MustReadNEvents

func MustReadNEvents(t *testing.T, r io.Reader, count int) []Event

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

func ReadEvents(r io.Reader) ([]Event, error)

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

func ReadNEvents(r io.Reader, count int) ([]Event, error)

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

func (e Event) DataValue(key string) string

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

func (e Event) Elements() string

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

func (e Event) IsElements() bool

IsElements reports whether the event type is datastar-patch-elements.

func (Event) IsScript

func (e Event) IsScript() bool

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) IsSignals

func (e Event) IsSignals() bool

IsSignals reports whether the event type is datastar-patch-signals.

func (Event) Mode

func (e Event) Mode() string

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

func (e Event) Namespace() string

Namespace returns the XML namespace for a patch-elements event. Returns "html" (the DataStar default) if no namespace dataline is present.

func (Event) OnlyIfMissing

func (e Event) OnlyIfMissing() bool

OnlyIfMissing reports whether the signals patch has the onlyIfMissing flag.

func (Event) ScriptContent

func (e Event) ScriptContent() string

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

func (e Event) Selector() string

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

func (e Event) SignalsJSON() []byte

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

func (e Event) String() 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

func (e Event) UnmarshalSignals(target any) error

UnmarshalSignals decodes the signals JSON payload from a patch-signals event into the target. The target must be a pointer.

func (Event) UseViewTransitions

func (e Event) UseViewTransitions() bool

UseViewTransitions reports whether the event enables the View Transition API.

func (Event) ViewTransitionSelector

func (e Event) ViewTransitionSelector() string

ViewTransitionSelector returns the scoped view transition selector, if any.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL