Skip to Content
steep

Program and messages

Send messages, change the model directly, quit, and check the messages your model receives.

Prefer keys and clicks, since they test what a user does. These are for what's hard to reach that way. They need a tea.Program, so an exec harness can't use them.

SendProgram

SendProgram sends a message straight to the program, as something outside it would, such as a tea.Program.Send from a background goroutine:

h.SendProgram(dataLoadedMsg{rows: rows}).Wait("42 rows")

Since input goes through the terminal, mixing the two can deliver them out of order. See Ordering.

Mutate

Mutate applies a function to the model on the program's goroutine, as part of its Update handling, so it's safe from races.

steep.NewHarness(t, newModel()).
	Wait("Sencha").
	Mutate(func(m *Model) *Model {
		m.Served = []string{"Matcha"}
		return m
	}).
	Wait("1 served")

The function must take and return the type you passed to the harness, or the test fails. So for a harness of a *Model, that's func(*Model) *Model.

Use it sparingly, for states that would take too long to reach through the UI, or that would otherwise need test-only code in your model. A test that mutates its way to every screen doesn't check that a user can get there.

QuitProgram

QuitProgram asks the program to shut down gracefully. You only need it to check what happens next, since the cleanup stops the program anyway:

m := h.QuitProgram().FinalModelAs[*Model]()

Sleep

Sleep lets the program run for a while, such as to hold on a screen in a recording.

h.Sleep(500 * time.Millisecond)

Don't use it to wait for something to happen. It either wastes time or isn't long enough on a slow machine. Wait for what you expect instead.

Reaching the session

Session returns the reel.Session your program runs in, for what the harness doesn't wrap, such as writing raw bytes to the terminal:

h.Session().Write([]byte(ansi.SetModeInBandResize))

Messages

The harness keeps every message your model receives. It's useful when the thing you want to check doesn't draw anything, or to see that a command ran. It needs a tea.Program, so an exec harness can't use it.

h := steep.NewHarness(t, newModel())

size := h.WaitMessage[tea.WindowSizeMsg]()
if size.Width != 80 {
	t.Errorf("width = %d", size.Width)
}

h.Key("enter")
h.WaitLiveMessage[brewDoneMsg]()
h.RequireHasMessage[tea.KeyPressMsg]()

Waiting

MethodConsidersReturns
WaitMessage[T]()History and later messagesThe first T
WaitMessageWhere(fn)History and later messagesThe first T that fn accepts
WaitLiveMessage[T]()Only messages after the callThe first T
WaitLiveMessageWhere(fn)Only messages after the callThe first T that fn accepts

Use WaitLive* to wait for the response to something you just did. WaitMessage might return a matching message from before.

T is inferred from fn in the Where forms:

msg := h.WaitMessageWhere(func(m tea.KeyPressMsg) bool {
	return m.String() == "enter"
})

Keep fn quick. A wait that falls 32 messages behind misses the newer ones.

Asserting

AssertHasMessage[T]() and RequireHasMessage[T]() check the history as it is, without waiting.

Reading them yourself

MethodReturns
MessageHistory()A copy of all messages so far
Messages(ctx)Past messages, then new ones until ctx is done
LiveMessages(ctx)New messages until ctx is done
FilterMessagesType[T]()History messages of type T
FilterMessagesFunc(fn)History messages of type T that fn accepts
for key := range h.FilterMessagesType[tea.KeyPressMsg]() {
	t.Log(key.String())
}

The iterators buffer 32 new messages. A loop that falls further behind misses the newer ones, which MessageHistory still has.

Recording them

To put messages in a recording, so the inspector can show them, see WithRecordMessages.

Last updated on

On this page