3.7 KiB
Driving a CLI / TUI (tmux)
Each scenario gets its own named tmux session (cleanup needs a deterministic name). Fix the size for deterministic capture; prefer the app's plain-text/inline mode if it has one.
The four-command recipe
tmux new-session -d -s <name> -x 200 -y 50 "<cmd> 2>/tmp/<name>-stderr.log"
tmux send-keys -t <name> -l "literal text" # -l = no key-name parsing (paths, slashes)
tmux send-keys -t <name> Enter
tmux capture-pane -t <name> -p # -p = plain text; add -e only for styling
-x 200 -y 50fixes the pane size socapture-paneoutput is deterministic run to run — a resized pane reflows text differently.- Always
-lfor user-typed strings; without it a literal path like/foo/bargets parsed as arrow-key escapes instead of typed characters. - Redirect stderr to a file — panics, log lines, and debug probes land there,
not in the pane, so they won't show up in a
capture-panesnapshot at all.
Kill any leftover session with the same name before starting a new one, so reruns don't attach to a stale process:
tmux kill-session -t <name> 2>/dev/null # idempotent: fine if nothing to kill
Form fill: send-keys patterns
send-keys parses keystrokes by name (Enter, BTab, C-u) unless you pass
-l for literal text. A typical field-by-field fill mixes both:
tmux send-keys -t <name> BTab # shift-tab to a prior field
tmux send-keys -t <name> C-u # clear the current line
tmux send-keys -t <name> -l "some/literal/path" # literal — no key parsing
tmux send-keys -t <name> Tab # forward to the next field
tmux send-keys -t <name> Enter
sleep 0.3 between keys is usually enough; bump to 0.5–1.0s for field
transitions where the UI re-renders.
Polling capture-pane for state
Poll capture-pane -p for a state string and grep the glyph or word, not
the color — -p drops ANSI styling by default (add -e only if you need
styling), and colors are also just harder to grep reliably than a fixed
glyph:
for i in $(seq 1 30); do
pane=$(tmux capture-pane -t <name> -p)
echo "$pane" | grep -q "state: processing" && break
sleep 1
done
TUIs commonly use a distinct glyph per state, e.g. a Braille spinner (⠋)
while pending and an X mark (✗) on failure, with the glyph simply removed
once reconciled. Grep for the glyph itself, not for a color code.
Two captures for optimistic UI
Mirror the web sync/async pattern: capture the pane immediately after the triggering keypress, then again after a reconcile window. Without the immediate capture you can't tell "rendered then reconciled" from "never rendered":
tmux send-keys -t <name> -l "trigger the optimistic action"
tmux send-keys -t <name> Enter
echo "=== synchronous ===" ; tmux capture-pane -t <name> -p | grep -E "pending-glyph"
sleep 6
echo "=== reconciled ===" ; tmux capture-pane -t <name> -p | grep -E "pending-glyph" || echo "[no pending — reconciled]"
Plain-text mode over the alt-screen buffer
If the TUI has a flag that disables its alternate-screen buffer (a debug or
plain-output mode), use it when launching under tmux. capture-pane then sees
plain scrollback text instead of raw escape sequences from a full-screen
redraw, which is much easier to grep.
Non-interactive CLIs don't need tmux
If the surface under test is a one-shot command rather than an interactive session, skip tmux entirely — run the command and capture its stdout/stderr directly. The tmux machinery exists for interaction, not for driving a binary in general. Still run it against a real, freshly built instance, not a stale one left over from an earlier session.