Skip to content

Terminal testing

Test an interactive terminal program by running it in a tape, waiting for expected output, and saving state JSON for assertions.

Output target/snapshots/app.gif
Type "my-app --demo"
Enter
Wait+Screen "Ready"
State target/snapshots/ready.json
Screenshot target/snapshots/ready.png

The tape fails if expected text does not appear before the configured timeout. The PNG helps with visual review, while State JSON can be compared in snapshot tests.

Prefer asserting text and important style spans instead of every pixel. Pixel assertions are useful for renderer work, but they are often brittle for application behavior tests.

Good assertions include:

  • expected viewport text
  • expected scrollback text
  • selected style spans for important colors or attributes
  • cursor visibility and location when it affects behavior
  • whether a prompt, status line, or selected row appears
  • whether hidden setup avoided extra visible frames

Use waits as synchronization points:

Type "my-tui --scripted"
Enter
Wait+Screen "Loaded 12 rows"

Prefer Wait+Screen for TUIs because cursor location may move as the program draws. Use regex waits for dynamic output:

Wait+Screen "/Loaded [0-9]+ rows/"

Use Env to make application behavior deterministic:

Env NO_COLOR 1
Env TZ UTC
Env BETAMAX_TEST 1

Use Set CursorBlink false when animation frame differences do not matter to the test.

After an action, use Wait, Wait+Line, or Wait+Screen to check the expected text before continuing. Add Sleep if viewers need time to read the recording.

Type "my-tui --scripted"
Enter
Wait+Screen "Loaded 12 rows"
State target/snapshots/loaded.json
Sleep 500ms

Keep validation sleeps short and rare. If a sleep is compensating for behavior that cannot be matched with visible text, a prompt, or a status line, keep it as small as the program allows and leave a tape comment explaining what external effect it covers.

To check a snapshot from a Rust test:

  1. Run betamax run tests/fixtures/profile.tape.
  2. Read target/snapshots/profile.json.
  3. Deserialize it with serde_json.
  4. Assert targeted fields directly, or snapshot the whole file with insta.

For tests that call Betamax directly from Rust, betamax-core exposes tape parsing, the runner, and terminal state types.

State JSON should be the primary assertion artifact. Screenshots are best as debugging attachments when a snapshot changes. GIFs are useful when reviewing interaction timing, but they should usually not be the only automated assertion.

See Examples for scrollback.tape, text-styles.tape, and outputs.tape, which exercise the most useful testing paths.