Documentation
¶
Overview ¶
Package vt (via test) holds testing helpers for via compositions. It lets tests drive a Composition by HTTP without parsing HTML, by name-addressing actions and signals through the descriptor.
app := via.New()
via.Mount[Counter](app, "/")
srv := vt.Serve(t, app)
tc := vt.NewClient(t, srv, "/")
tc.Action(p.Inc).WithSignal("step", 3).Fire()
require.Contains(t, tc.Reload(), ">3<")
Index ¶
- func AwaitFrame(t testing.TB, frames <-chan string, timeout time.Duration, needles ...string) string
- func Serve(t testing.TB, app *via.App) *httptest.Server
- func TabIDFromHTML(html string) string
- type ActionCall
- type Client
- func (c *Client) Action(target any) *ActionCall
- func (c *Client) Fork(path string) *Client
- func (c *Client) HTML() string
- func (c *Client) Reload() string
- func (c *Client) SSE() (frames <-chan string, cancel func())
- func (c *Client) SSEReady() (frames <-chan string, cancel func())
- func (c *Client) TabID() string
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AwaitFrame ¶
func AwaitFrame(t testing.TB, frames <-chan string, timeout time.Duration, needles ...string) string
AwaitFrame waits for every needle to appear on a single SSE frames channel, failing the test if any one is missing within timeout. Returns the accumulated frame content at the moment the match landed, so callers can assert further on what came in alongside.
frames, cancel := tc.SSE()
defer cancel()
tc.Action("Bump").Fire()
body := vt.AwaitFrame(t, frames, 2*time.Second, "<div>3</div>")
assert.NotContains(t, body, "stale")
func Serve ¶ added in v0.6.0
Serve starts an httptest.Server bound to app and registers its shutdown with t.Cleanup, so a test gets a live URL in one line:
app := via.New() via.Mount[Counter](app, "/") srv := vt.Serve(t, app) tc := vt.NewClient(t, srv, "/")
app is its own http.Handler, so the server dispatches through App.ServeHTTP on every request — routes mounted before or after Serve are both reachable.
func TabIDFromHTML ¶ added in v0.6.0
TabIDFromHTML extracts the via_tab id from a rendered page's data-signals meta (the id is `<route>_<64-hex>`). The HTML escapes the JSON quotes, so tests reaching for the tab id directly should use this rather than hand-rolling a regex against the escaped form. Returns "" if absent.
Types ¶
type ActionCall ¶
type ActionCall struct {
// contains filtered or unexported fields
}
ActionCall is a builder for action invocations.
func (*ActionCall) Fire ¶
func (a *ActionCall) Fire() int
Fire issues POST /_action/{name} and returns the response status code.
func (*ActionCall) WithFile ¶
func (a *ActionCall) WithFile(field, filename string, body []byte) *ActionCall
WithFile attaches a file part to the action POST. Adding any file switches the request from JSON to multipart/form-data; signals added via WithSignal ride along as text fields. Repeat calls add multiple files.
tc.Action(p.Upload).
WithFile("avatar", "me.png", pngBytes).
WithSignal("note", "from CLI").
Fire()
func (*ActionCall) WithSignal ¶
func (a *ActionCall) WithSignal(name string, value any) *ActionCall
WithSignal adds a signal value to send with the action POST.
A `_`-prefixed name is a Datastar client-only (local) signal, which a real browser never POSTs to the server — sending one here reproduces behavior that cannot happen in the browser, so WithSignal logs a (non-fatal) warning.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client drives a mounted Composition over HTTP for tests.
func NewClient ¶
NewClient performs a GET on path, picks up the rendered tab id, and is ready to drive actions and signal updates against that tab.
func (*Client) Action ¶
func (c *Client) Action(target any) *ActionCall
Action returns a handle that fires an action. The target may be either the action's name as a string, or a bound method value whose method name is resolved via the runtime — the typed form gives the test compile-time protection against typos:
tc.Action("Bump").Fire() // string form
tc.Action(p.Bump).Fire() // typed form (preferred)
func (*Client) Fork ¶
Fork opens a second tab against path that shares this client's cookie jar, so both tabs land on the same session — the only way to drive StateSess behavior that spans tabs.
func (*Client) Reload ¶
Reload re-fetches the currently mounted page, refreshes lastBody, and returns the new HTML. Use after firing actions to assert on the re-rendered body — server-side state changes only show up in HTML on a fresh GET (or via the SSE stream, but that's heavier to wire up).
tc.Action("Bump").Fire()
body := tc.Reload()
assert.Contains(t, body, ">3<")
Note: Reload assigns a *new* tab id (each GET registers a fresh Ctx). If you need to assert against the original tab, capture HTML() first.
func (*Client) SSE ¶
SSE opens an SSE stream and returns a cancel func and a channel of frames. Use only when you must observe live patch frames.
The stream uses a separate http.Client with no overall timeout — the regular client's Timeout would kill the stream mid-flight. Per-frame waits should be bounded with AwaitFrame; cancel with the returned func.
func (*Client) SSEReady ¶
SSEReady opens an SSE stream and blocks until the server's handshake comment (`: ready`) arrives, signalling that the server-side SSE goroutine has entered its select loop and is registered to receive patch-queue wakeups. Use it in place of the `tc.SSE(); time.Sleep(20*time.Millisecond)` idiom — it replaces a timing guess with a deterministic wait so the suite doesn't flake on busy CI runners. The comment bytes are consumed off the channel so downstream `AwaitFrame` calls see only post-handshake traffic. Times out after 2s.