vt

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Jun 12, 2026 License: MIT Imports: 17 Imported by: 0

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

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

func Serve(t testing.TB, app *via.App) *httptest.Server

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

func TabIDFromHTML(html string) string

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

func NewClient(t testing.TB, server *httptest.Server, path string) *Client

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

func (c *Client) Fork(path string) *Client

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

func (c *Client) HTML() string

HTML returns the most recently fetched page body.

func (*Client) Reload

func (c *Client) Reload() string

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

func (c *Client) SSE() (frames <-chan string, cancel func())

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

func (c *Client) SSEReady() (frames <-chan string, cancel func())

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.

func (*Client) TabID

func (c *Client) TabID() string

TabID returns the active tab id.

Jump to

Keyboard shortcuts

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