Skip to Content
steep

Waits and checks

The three ways to check the screen, how waiting and settling work, and conditions beyond plain text.

Every check in steep follows one naming convention, so the name tells you what happens if it fails.

PrefixIf the check failsBlocks?Use it for
Wait*Stops the test, after the timeoutYes, until it holdsAnything the program still has to do
Assert*Reports the failure, and the test carries onNoChecking several things about a finished screen
Require*Stops the test immediatelyNoA check that the rest of the test depends on

The same failure, three ways

Here's how a test goes on after each kind of failure:

A recording that continues after two failed assertions and lists both on the FAIL card
Assert: the test carries on, and the card lists both failures.Open in the inspector ↗
A recording that stops at the first failed check
Require: the test stops at the first.Open in the inspector ↗
A recording that waits for a tea that never appears, then fails
Wait: the test gives the program time, then stops.Open in the inspector ↗

With Assert, both missing teas are reported and the steps after them still run:

h.Wait("Sencha").
	Assert("Genmaicha").
	Assert("Hojicha").
	KeyDown().
	Assert("Matcha")

With Require, the first failure ends the test, so Hojicha isn't checked:

h.Wait("Sencha").
	Require("Genmaicha").
	Assert("Hojicha").
	KeyDown()

Failures shows what each of these prints.

Choosing

Most of a test should be Wait. A program draws at its own pace, and the check after an input usually runs before the program has redrawn. Assert right after Key is a race, so a test that does that passes on your machine and fails in CI.

Use Assert once the screen has settled. After a Wait, the screen has what you waited for, and an Assert can check the rest of it:

h.Key("enter").
	Wait("Order placed").
	Assert("Matcha").
	AssertNot("Error")

Use Require when carrying on would be pointless. If a dialog isn't open, the next ten steps will only fail with confusing messages.

The checks

Each of these has an Assert and a Require form. On a harness, they return the harness for chaining.

CheckHolds when
Assert(content)The text is on the screen. With a []string, every one of them is.
AssertNot(content)The text isn't. With a []string, none of them is.
AssertFor(cond)A condition holds
AssertWidth(n), AssertHeight(n), AssertDimensions(w, h)The output has that size
AssertHasMessage[T]()The model has received a T (see Messages)
AssertSnapshot(), AssertJSON()The screen matches a snapshot

The Require forms are Require, RequireNot, RequireFor, RequireWidth, RequireHeight, RequireDimensions, RequireHasMessage, RequireSnapshot, and RequireJSON.

The Wait family blocks on the same kinds of thing: Wait(content), WaitNot(content), WaitFor(cond), WaitSettle(), WaitSettleMessages(), WaitMessage[T](), and WaitFinished().

Checks without a harness

The package also has functions of the same names, which check anything with a func() string, so you can use them on a model's output without running a program. Assert returns whether it passed:

func TestView(t *testing.T) {
	view := func() string { return renderMenu(menu) }

	if steep.Assert(t, view, "Sencha") {
		steep.AssertWidth(t, view, 40)
	}
	steep.Require(t, view, []string{"Sencha", "Matcha"})
}

Options

Every check takes options for that call only:

h.Wait("Matcha is ready", steep.WithTimeout(10*time.Second))

steep.WithANSI(false) strips ANSI from a view you pass to the package's functions, such as steep.Assert(t, view, "Sencha", steep.WithANSI(false)). A harness checks the screen's text, which has none.

Waiting and settling

Your program runs concurrently with your test. After Key("enter") returns, the key has been sent, but your program may not have handled it, let alone redrawn. So a test has to wait for what it expects.

Wait for what you expect

Wait blocks until text is on the screen, and returns as soon as it is. It's the check to reach for first.

h.Key("enter").Wait("Order placed")

It doesn't poll. Screen waits are woken each time the terminal changes, so a wait returns right after the redraw, and doesn't cost anything while nothing happens.

WaitUntil
Wait(content)The text is on the screen. With a []string, all of them are.
WaitNot(content)The text is gone. With a []string, all of them are.
WaitFor(cond)A condition holds
Locate(cond)The same, and it returns where the match is

If it doesn't happen within the timeout, the test stops, and steep prints what was on the screen:

See what that looks like.

Settling

Sometimes there's no text to wait for. You pressed a key that should change nothing, or a screen's only difference is a color. For those, WaitSettle waits until the screen's text has stopped changing:

h.Key("enter").
	WaitSettle(). // The brew redraws every 60ms, so this returns once it's done.
	Assert("Sencha is ready")

It returns once the screen's text hasn't changed for the settle period, 100ms by default, counting from the call. Each change restarts the count.

screen changes
WaitSettle
100ms quiet
WithTimeout
A progress bar filling while the brew runs, then the status changes to ready
WaitSettle returns once the brew's redraws stop.

There are three things to know about it:

  • A change can come after the period. If your program does something slow before it redraws, the screen may look settled before the change arrives. Prefer Wait when you know what to look for.
  • A repaint isn't a change. Only the screen's text counts, so a color change or a redraw of the same text doesn't restart the period.
  • It never returns if the program never stops. A spinner or a clock keeps the text changing, so the wait fails at the timeout.

Settling on messages

WaitSettleMessages waits until the model hasn't received a message for the settle period. That's useful for programs that are still working but not drawing, or for a screen with an animation that never stops. You can tell it which messages don't count:

h := steep.NewHarness(t, newModel(), steep.WithSettleIgnoreMsgs(spinner.TickMsg{}))
h.Key("enter").WaitSettleMessages()

Pass a value of each type to ignore. Types have to match exactly. It needs a tea.Program, so an exec harness can't use it.

Waiting for messages

To wait for a specific message rather than a quiet period, use WaitMessage.

Which to use

YesNoYesNo, it animatesYesNoDo you know what will beon the screen?Wait or WaitForDoes the screen stopchanging?WaitSettleDo you know the messagethat ends it?WaitMessageWaitSettleMessages withWithSettleIgnoreMsgs

The timers

OptionVariableDefaultControls
WithTimeoutSTEEP_TIMEOUT2sHow long any blocking wait may take: startup, waits, settles, and mutations
WithSettleSTEEP_SETTLE100msThe quiet period. Capped at 80% of the timeout.
WithCheckSTEEP_CHECK10msHow often WaitSettleMessages re-checks. Screen waits don't poll.
WithSlowSTEEP_SLOWOffA sleep after every action, for programs that need real time between them

A variable overrides its option unless the harness has WithIgnoreEnv(true). So a slow CI machine can be handled with no code changes:

STEEP_TIMEOUT=30s go test ./...

You can pass any of these to one call, such as h.Wait("Report ready", steep.WithTimeout(time.Minute)).

WithSlow isn't needed for recordings, which play back the timeline as it happened. It's for programs that need time between actions, or for slowing a demo down so it can be followed.

Don't sleep

// Avoid: wastes a second, and fails on a machine that needs two.
h.Key("enter").Sleep(time.Second).Assert("Order placed")

// Better: returns as soon as it's true.
h.Key("enter").Wait("Order placed")

Conditions

WaitFor, AssertFor, RequireFor, and Locate take a reel.Condition, which is more flexible than text.

h.WaitFor(reel.Matches(regexp.MustCompile(`\d+ rows`)))
h.AssertFor(reel.NotContains("Error"))
h.Locate(reel.Contains("[ Save ]")).LeftClick()
ConditionHolds whenLocates
reel.Contains(texts...)Every text is on the screenWhere each first appears
reel.NotContains(texts...)None of them isNothing
reel.Matches(re)The regular expression matchesWhere it first matches
reel.NotMatches(re)It doesn't matchNothing
reel.ScreenFunc(fn)fn returns true for the screen's textNothing
reel.OnCursorLine(c)c holds for the cursor's line aloneWhat c locates

The cursor's line

OnCursorLine waits for a prompt without being fooled by the same text earlier on the screen. That's mostly useful for shells and programs that print rather than take over the screen:

h := steep.NewExecHarness(t, exec.Command("bash", "--norc"))
h.WaitFor(reel.OnCursorLine(reel.Matches(regexp.MustCompile(`\$ $`))))

It doesn't hold while the viewport is scrolled away from the cursor.

Your own conditions

ScreenFunc is for anything else. It's only checked when the screen changes, so it should depend on the text alone:

h.WaitFor(reel.ScreenFunc(func(text string) bool {
	return strings.Count(text, "✓") == 3
}))

A type with Check and String methods is a condition too, and its matches let Locate point at what it found. See reel.Condition.

Text and ANSI

Screen conditions see the screen's text, with no escape sequences. So reel.Contains("Matcha") matches wherever that word is drawn, in any color. To check a color or style, use a JSON snapshot, or a text snapshot with its ANSI sequences kept.

Last updated on

On this page