vtermtest

package module
v0.0.0-...-a9fdf7f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2025 License: MIT Imports: 13 Imported by: 0

README

vtermtest

Snapshot testing for interactive TUIs/REPLs (e.g., go-prompt) using a real PTY + libvterm. Feed keystrokes (printable + special keys) and capture the rendered screen as plain text at any step.

Usage

Installation
go get github.com/c-bata/vtermtest
How to install libvterm
$ brew install libvterm
or
$ apt install libvterm-dev
Command-line Interface
$ vtermtest-cli --command kube-prompt --keys "get p<Tab>"
kube-prompt v1.0.11 (rev-ac5964a)
Please use `exit` or `Ctrl-D` to exit this program.
>>> get p
          persistentvolumeclaims
          persistentvolumes
          pod
          podsecuritypolicies
          podtemplates
          pvc
Command-line Options
$ vtermtest-cli --help
vtermtest-cli - Terminal emulator testing tool

USAGE:
    vtermtest-cli --command "COMMAND" [OPTIONS]

OPTIONS:
    --command STRING    Command to execute (required)
    --keys STRING       Key sequence in DSL format
    --rows INT          Terminal rows (default: 24)
    --cols INT          Terminal columns (default: 80)
    --output FILE       Output file (default: stdout)
    --timeout DURATION  Total timeout for command execution (default: 30s)
    --stable-duration DURATION  Duration screen must remain unchanged (default: 200ms)
    --stable-timeout DURATION   Timeout for screen stabilization (default: 10s)
    --env STRING        Environment variables (KEY=VALUE,...)
    --dir STRING        Working directory
    --delimiter STRING  DSL tag delimiters (default: "<>")
    --raw-output        Output raw bytes from PTY instead of rendered screen
    --raw-format STRING Raw output format: binary, hex, escaped (default: binary)

KEY DSL:
    Text: hello world
    Keys: <Tab> <Enter> <BS> <Del> <Esc> <Space> <Up> <Down> <Left> <Right>
    Ctrl: <C-a> ... <C-z>  Alt: <A-a> ... <A-z>  Fn: <F1> ... <F24>
    Nav: <Home> <End> <PageUp> <PageDown>
    Wait: <WaitStable> <WaitFor text>
    Escape: << (literal <)
Go API
package myapp_test

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/c-bata/vtermtest"
	"github.com/c-bata/vtermtest/keys"
)

func Test_GoPrompt_InlineAssert(t *testing.T) {
	// rows, cols
	emu := vtermtest.New(4, 50).
		Command("go", "run", "./_examples/go_prompt_example.go").
		Env("LANG=C.UTF-8")
	t.Cleanup(func() { _ = emu.Close() })

	if err := emu.Start(context.Background()); err != nil {
		t.Fatalf("start: %v", err)
	}

	// Type query + press Tab to trigger completion
	if err := emu.KeyPress(keys.Text("SELECT * FROM "), keys.Tab); err != nil {
		t.Fatalf("send: %v", err)
	}

	// Assert lines (row index starts at 0)
	emu.AssertLineEqual(t, 0, ">>> SELECT * FROM articles")
	emu.AssertLineEqual(t, 1, "                   users     user table")
	emu.AssertLineEqual(t, 2, "                   articles  articles table")
	emu.AssertLineEqual(t, 3, "") // empty line

	// Or assert the whole screen
	emu.AssertScreenEqual(t, `
>>> SELECT * FROM articles
                   users     user table
                   articles  articles table

`)
}
Golden/Snapshot Test
package myapp_test

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/c-bata/vtermtest"
	"github.com/c-bata/vtermtest/keys"
)

func TestSnapshotGoPrompt(t *testing.T) {
	emu := vtermtest.New(4, 50).
		Command("go", "run", "_examples/go_prompt_example.go").
		Env("LANG=C.UTF-8")
	t.Cleanup(func() { _ = emu.Close() })

	if err := emu.Start(context.Background()); err != nil {
		t.Fatalf("start: %v", err)
	}

	_ = emu.KeyPress(keys.Text("SELECT * FROM "), keys.Tab);
	if !emu.WaitStable(50*time.Millisecond, 2*time.Second) {
		t.Fatalf("screen did not stabilize")
	}
	screen, err := emu.GetScreenText()
	if err != nil {
		t.Fatalf("get screen: %v", err)
	}

	// Golden comparison
	golden := filepath.Join("testdata", "sql_example.golden.txt")
	if os.Getenv("VTERMTEST_GOLDEN_UPDATE") == "1" {
		if err := os.WriteFile(golden, []byte(screen), 0o644); err != nil {
			t.Fatalf("write golden: %v", err)
		}
	} else {
		expect, err := os.ReadFile(golden)
		if err != nil {
			t.Fatalf("read golden: %v", err)
		}
		if string(expect) != screen {
			t.Fatalf("screen mismatch.\n--- want ---\n%s\n--- got ---\n%s", expect, screen)
		}
	}
}
Keys API
Keys
// Text & printable
keys.Text("use")

// Common keys
keys.Tab
keys.Enter
keys.Backspace
keys.Up, keys.Down, keys.Left, keys.Right
keys.Home, keys.End, keys.PageUp, keys.PageDown
keys.Delete

// Fn Keys
keys.F(1) ... keys.F(24)

// Ctrl keys
keys.CtrlA, keys.CtrlB, keys.CtrlC
DSL
// Simple text with special keys
emu.KeyPressString("SELECT * FROM us<Tab>")

// Vim-like operations
emu.KeyPressString("ihello world<Esc>:wq<Enter>")

// Complex editing
emu.KeyPressString("<C-a>deleted<C-k>new text<Enter>")

// Escaped angle brackets
emu.KeyPressString("echo <<literal angle bracket>>")

DSL Notation:

  • Regular text: typed as-is
  • Special keys: <Tab> <Enter> <BS> <Del> <Esc> <Space>
  • Arrow keys: <Up> <Down> <Left> <Right>
  • Ctrl keys: <C-a> ... <C-z>
  • Alt keys: <A-a> ... <A-z>
  • Function keys: <F1> ... <F24>
  • Navigation: <Home> <End> <PageUp> <PageDown>
  • Escape: << for literal <

Limitations

  • This library currently focuses on characters; color/attributes are not included (by design).
  • Tested on Linux/macOS; Windows support depends on your PTY backend and toolchain.
  • Requires CGO and a libvterm toolchain supported by your OS.

Acknowledgements

  • libvterm — An abstract library implementation of a VT220/xterm/ECMA-48 terminal emulator.
  • mattn/go-libvterm — Go bindings used internally.
  • creack/pty — PTY management.

Documentation

Overview

Package vtermtest provides snapshot testing for interactive TUIs/REPLs using a real PTY and libvterm.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Emulator

type Emulator struct {
	// contains filtered or unexported fields
}

Emulator represents a terminal emulator for testing interactive programs. It creates a PTY, launches a process, and uses libvterm to emulate terminal behavior.

func New

func New(rows, cols uint16) *Emulator

New creates a new Emulator with the specified terminal dimensions. rows and cols specify the terminal size in characters.

func (*Emulator) AssertLineEqual

func (e *Emulator) AssertLineEqual(t TestingT, row int, want string)

AssertLineEqual asserts that a specific line equals the expected string. It retries with exponential backoff until the assertion passes or max attempts is reached.

func (*Emulator) AssertScreenContains

func (e *Emulator) AssertScreenContains(t TestingT, substr string)

AssertScreenContains asserts that the screen contains the given substring.

func (*Emulator) AssertScreenEqual

func (e *Emulator) AssertScreenEqual(t TestingT, want string)

AssertScreenEqual asserts that the entire screen matches the expected string. Leading/trailing whitespace in want is trimmed, and empty lines at the start are ignored.

func (*Emulator) Close

func (e *Emulator) Close() error

Close terminates the process and cleans up resources. It closes the PTY, kills the process if still running, and waits for cleanup.

func (*Emulator) Command

func (e *Emulator) Command(path string, args ...string) *Emulator

Command sets the command to execute. Returns self for method chaining.

func (*Emulator) Dir

func (e *Emulator) Dir(dir string) *Emulator

Dir sets the working directory for the command. Returns self for method chaining.

func (*Emulator) EnableRawBytesCollection

func (e *Emulator) EnableRawBytesCollection() *Emulator

EnableRawBytesCollection enables collection of raw bytes from PTY. When enabled, all bytes read from PTY are stored and can be retrieved with GetRawBytes().

func (*Emulator) Env

func (e *Emulator) Env(env ...string) *Emulator

Env adds environment variables. Multiple calls append variables. Format: "KEY=value". Returns self for method chaining.

func (*Emulator) GetCursorPosition

func (e *Emulator) GetCursorPosition() (row, col int, err error)

GetCursorPosition returns the current cursor position from libvterm's internal state. Returns the 1-based row and column position.

func (*Emulator) GetLine

func (e *Emulator) GetLine(row int) (string, error)

GetLine returns a specific line from the terminal screen. Row index starts at 0. Trailing spaces are trimmed.

func (*Emulator) GetRawBytes

func (e *Emulator) GetRawBytes() []byte

GetRawBytes returns the raw bytes collected from PTY. Raw bytes collection must be enabled with EnableRawBytesCollection(). Returns a copy of the collected bytes.

func (*Emulator) GetScreenText

func (e *Emulator) GetScreenText() (string, error)

GetScreenText returns the entire terminal screen as a string. Lines are trimmed of trailing spaces and joined with newlines.

func (*Emulator) KeyPress

func (e *Emulator) KeyPress(keys ...[]byte) error

KeyPress sends keystrokes to the terminal. Use the keys package for special keys (e.g., keys.Tab, keys.Enter).

func (*Emulator) KeyPressString

func (e *Emulator) KeyPressString(dsl string) error

KeyPressString sends keystrokes using DSL notation. Example: "hello<Tab>world<C-c>" sends "hello", Tab key, "world", then Ctrl-C. Special DSL: <WaitStable> waits for screen to stabilize. See keys.Parse for supported notation.

func (*Emulator) KeyPressStringWithOptions

func (e *Emulator) KeyPressStringWithOptions(dsl string, opts keys.ParseOptions) error

KeyPressStringWithOptions sends keystrokes using DSL notation with custom tag delimiters. Example with options {TagStart: '[', TagEnd: ']'}: "hello[Tab]world[C-c]"

func (*Emulator) Resize

func (e *Emulator) Resize(rows, cols uint16) error

Resize changes the terminal size dynamically. Both PTY and libvterm are resized to match the new dimensions.

func (*Emulator) Start

func (e *Emulator) Start(ctx context.Context) error

Start launches the command in a PTY and begins terminal emulation. The context can be used to control the lifetime of the process.

func (*Emulator) WaitFor

func (e *Emulator) WaitFor(text string, timeout time.Duration) error

WaitFor waits until the specified text appears on the screen. Returns error if text doesn't appear within timeout. timeout: maximum time to wait for the text to appear

func (*Emulator) WaitStable

func (e *Emulator) WaitStable(quiet, timeout time.Duration) bool

WaitStable waits until the screen output is stable (no changes for 'quiet' duration). Returns true if stable within timeout, false if timeout exceeded. quiet: duration of inactivity to consider stable timeout: maximum time to wait

func (*Emulator) WithAssertBackoffFactor

func (e *Emulator) WithAssertBackoffFactor(f float64) *Emulator

WithAssertBackoffFactor sets the backoff multiplier for retry delays

func (*Emulator) WithAssertInitialDelay

func (e *Emulator) WithAssertInitialDelay(d time.Duration) *Emulator

WithAssertInitialDelay sets the initial delay between retry attempts

func (*Emulator) WithAssertMaxAttempts

func (e *Emulator) WithAssertMaxAttempts(n int) *Emulator

WithAssertMaxAttempts sets the maximum number of retry attempts for assertions

type TestingT

type TestingT interface {
	Helper()
	Fatalf(format string, args ...interface{})
}

TestingT is the subset of testing.T used by assertions

Directories

Path Synopsis
cmd
vtermtest-cli command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL