tui-test for Python

September 18, 2026 ยท View on GitHub

Control, inspect, and test terminal apps from Python.

Install

pip install --pre tui-test

Python 3.8+ is supported.

Quick start

from tui_test import TuiTest

async with TuiTest.ephemeral() as terminal:
    await terminal.run("my-app")
    await terminal.get_by_text("Ready").expect()
    await terminal.get_by_text("Continue").click()
    await terminal.get_by_text("Done").expect()

API

TuiTest

TuiTest(session=None, *, backend=None, timeouts=None, profile=None, screen_history_limit=None, artifacts=None, recording=None)
OptionTypeDefault
sessionstrTUI_TEST_SESSION or "default"
backend"alacritty" | "ghostty" | "rio" | "xtermjs""alacritty"
timeoutsTimeouts | dictbuilt-in defaults
profileProfile | dictbuilt-in profile
screen_history_limitint | Nonecore default
artifactsdictoff
recordingAutomaticRecording | dict (directory only)default recording directory
traceTraceOptions | dict{"mode": "off", "directory": ".tui-test/traces"}

Set trace.mode to "on" for every session or "on-failure" for failures. Users can open trace.html; agents should read trace.md, trace.json, and timeline.json.

artifacts["on_failure"] selects "none", "text", "html", or "all". Use include_recording to include the cast in failure artifacts.

Properties

PropertyType
sessionstr
keyboardkeyboard helper
mousemouse helper

Lifecycle

MethodDescription
TuiTest.ephemeral(prefix=None, **options)Create a unique session.
await open(**options)Open a shell.
await run(program, *args, **options)Run a program.
await restart(graceful_timeout=5000)Restart the session.
await close()Close the session.
await close_quiet()Close without raising.
async with TuiTest()Close on exit.

open() options are shell, backend, cols, rows, cwd, env, wait_ready, restart, retries, profile, and timeouts. run() accepts the same options except shell.

The default size is 80 by 30. Timeout defaults are 5 seconds for text and idle, and 30 seconds for command, exit, and ready.

Input

MethodDescription
await submit(text=None)Type text and press Enter.
await type(text)Type text.
await write(data)Write raw bytes.
await press(*keys)Alias for keyboard.press().
await resize(cols, rows)Resize the terminal.
await signal(name)Send INT, TERM, KILL, or QUIT.
await kill()Kill the child process.

State

MethodReturns
await state()State
await text(full=False)str
await cells(x, y, w=1, h=1)list[Cell]
await get_command()str | None
await get_output()str | None
await get_exit_code()int | None
await get_cwd()str | None
await get_cursor()dict
await get_size()dict
await get_title()str | None
await get_clipboard()str
await get_bell_count()int
await get_bell_events()list[BellEvent]

Waits and assertions

MethodDescription
await wait_title(text, regex=False, not_=False, timeout=None)Wait for a title.
await wait_clipboard(text=None, timeout=None)Wait for a clipboard change or match.
await wait_idle(timeout=None)Wait for the screen to stop changing.
await wait_command(timeout=None)Wait for a submitted command.
await wait_exit(timeout=None)Wait for the program to exit.
await wait_ready(timeout=None)Wait for a shell prompt.
await wait_bell(timeout=None)Wait for a bell.
await expect_title(text, regex=False, not_=False, timeout=None)Assert the title.
await expect_exit_code(code, timeout=None)Assert the last exit code.
await expect_output(text, regex=False)Assert command output.
await expect_bell_count(count, timeout=None)Wait until the cumulative bell count reaches count.
await expect_snapshot(name, **options)Assert or update a snapshot.

wait_clipboard() waits for the next change. A string matches text. A compiled re.Pattern matches a regular expression.

Snapshot options are update, include_style, and include_title.

Capture

MethodDescription
await screenshot(path=None, full=False, zoom=None, background=None, transparent=False)Return text or save SVG or PNG.
await start_recording(path, **options)Start APNG, GIF, MP4, or asciinema recording.
await stop_recording()Finish the recording and return its path.

Recording options: format, fps, speed, idle_time_limit, zoom, background, and transparent. MP4 requires ffmpeg and does not support transparency.

The extension selects the format: .png or .apng, .gif, .mp4, or .cast. format overrides it.

Locator

Locators resolve against the latest terminal screen before every read or action.

from tui_test import TextStyle

save = (
    terminal
    .get_by_text("Settings")
    .get_by_text("Save", direction="after")
    .get_by_style(TextStyle(foreground="green"))
    .unique()
)

await save.click()

Create a locator

MethodOptions
terminal.get_by_text(text, **options)regex, full, whitespace
terminal.get_by_style(style, **options)full
terminal.get_by_link(uri, **options)full
locator.get_by_text(text, **options)regex, full, whitespace, direction
locator.get_by_style(style, **options)full, direction
locator.get_by_link(uri, **options)full, direction

whitespace is "exact" or "normalize". direction is "within", "after", or "before".

TextStyle fields are foreground, background, bold, dim, italic, underline_style, underline_color, inverse, hidden, strikethrough, and blink.

get_by_link(uri) matches an exact OSC 8 target, not visible URL text; get_by_link("") requires no link. Root style/link selectors find runs within each row. Chained calls with the default within direction check whole matches. Styles skip blanks if visible text exists; links check every cell.

Compose locators

link = terminal.get_by_link("https://example.com")
bold = terminal.get_by_style(TextStyle(bold=True))
bold_link_cells = bold.and_(link)
either = link.or_(terminal.get_by_text("Help"))
sections = terminal.get_by_text("Docs and Help")
contains_link = sections.filter(has=link)
without_old_text = sections.filter(has_not=terminal.get_by_text("old"))
entirely_linked = sections.get_by_link("https://example.com")

and_() keeps shared cells; or_() combines cells without duplicates. Adjacent cells merge within each physical row, even across original matches. Gaps and row breaks split runs. Counts and clicks use these runs; text keeps exact whitespace.

filter accepts only locators. has requires a match inside each candidate; has_not requires none. Both conditions apply when supplied, and the inner match may cover the whole candidate. For partially linked "Docs", filter(has=link) keeps the whole word, get_by_link(uri) rejects it, and and_(link) returns its linked cells.

Use locators from one TuiTest. Composition leaves them unchanged and reads one fresh snapshot when used. Selection order matters: a.first().and_(b) differs from a.and_(b).first(). Any full branch includes scrollback for the whole query. Errors propagate.

Select matches

MethodDescription
any()Keep all matches.
unique()Require one match.
first()Select the first match.
last()Select the last match.
nth(index)Select a zero-based match.

Read and act

MethodDescription
await locations()Return all selected locations.
await location()Return one location.
await count()Return the current count.
await all()Return one locator per current match.
await wait(state="visible", timeout=None)Wait for "visible" or "hidden".
await expect(not_=False, timeout=None)Assert the locator.
await click(**options)Click the middle cell.
await highlight(timeout=None)Highlight matches.

click() accepts button, alt, ctrl, shift, clicks, and timeout. button is "left", "middle", or "right".

location() and click() require one match. all() does not wait.

Keyboard

MethodDescription
await keyboard.press(*keys)Press keys.
await keyboard.down(*keys)Send keydown events.
await keyboard.repeat(*keys)Send repeat events.
await keyboard.up(*keys)Send keyup events.
await terminal.keyboard.press("Ctrl+C")
await terminal.keyboard.press("Escape", ":", "w", "q", "Enter")

Named keys include Up, Down, Left, Right, Home, End, PageUp, PageDown, Insert, Delete, Backspace, Tab, Enter, Space, Escape, and F1 through F12. Join modifiers such as Ctrl, Alt, Shift, Super, Meta, or Hyper with +.

Mouse

Coordinates are zero-based terminal cells.

MethodDescription
await mouse.click(x=None, y=None, **options)Click a cell or on_text.
await mouse.move(x, y)Move the pointer.
await mouse.down(x, y, **options)Press a button.
await mouse.up(x, y, **options)Release a button.
await mouse.drag(x1, y1, x2, y2, **options)Drag between cells.
await mouse.scroll("up" | "down", amount=3)Scroll.

Button options are button, alt, ctrl, and shift. Click also accepts on_text and clicks.

await terminal.mouse.click(10, 5, button="right", ctrl=True)

Module functions

FunctionDescription
await sessions()List sessions in this process.
await close_all()Close all sessions in this process.
await get_recording(session=None)Return an automatic asciinema recording.
unique_session(prefix=None)Create a unique session name.

Test helpers

Import from tui_test.testing.

FunctionDescription
await create_terminal(**options)Create, open, and track a terminal.
async with terminal(**options)Open and close a terminal.
await close_all_tracked()Close tracked terminals.
set_terminal_defaults(**options)Set suite defaults.
reset_terminal_defaults()Reset suite defaults.
track_terminal(terminal)Track a terminal.
untrack_terminal(terminal)Stop tracking a terminal.
tracked_count()Count tracked terminals.
terminal_snapshot(text)Normalize text for snapshots.

TerminalOptions adds shell, program, session, and prefix to the client and spawn options.

DEFAULT_SHELL is the platform default.

from tui_test.testing import terminal

async with terminal(program=("my-app",)) as app:
    await app.get_by_text("Ready").expect()

Configuration

from tui_test import AutomaticRecording, Colors, Profile, Timeouts

terminal = TuiTest(
    profile=Profile(
        scrollback=500,
        colors=Colors(foreground="#ffffff", background="#000000"),
    ),
    timeouts=Timeouts(text=10_000, command=60_000),
    artifacts={"dir": "artifacts", "on_failure": "all"},
    trace={"mode": "on-failure", "directory": "artifacts/traces"},
    recording=AutomaticRecording(directory="artifacts/casts"),
)

Types

TypeDescription
StateSession state and visible text.
CellOne terminal cell and its style.
TextMatchMatched text, positions, and spans.
TextStyleLocator style fields.
ProfileScrollback and colors.
TimeoutsText, idle, command, exit, and ready timeouts.
AutomaticRecordingAutomatic recording mode and directory.
ColorsTerminal palette.
MouseButton"left", "middle", or "right".
TextPosition, TextSpanMatch coordinates.
FailureDetailsStructured operation, locator, process, runtime, and screen evidence.
FailureArtifactRefPaths and write status for a failure artifact.

__version__ contains the package version.

Errors

ErrorExit code
ExpectationError1
UsageError2
NoSessionError3
InternalError5

All errors extend TuiTestError. Structured native failures expose details and artifact; expectation errors continue to populate compatibility terminal.text and terminal.screenshot fields. Failure artifacts can contain terminal output, titles, locator operands, and recordings, so review them before uploading.

Sessions are local to the current process and cannot be controlled by the CLI. Cancelling a task does not stop an active terminal operation.