charmtest

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

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

Go to latest
Published: Apr 1, 2026 License: MIT Imports: 7 Imported by: 0

README

charm-test

Testing framework for Bubble Tea applications.

Write deterministic, fast tests for your TUI apps — no terminal required.

Leia em Português

The Problem

Testing Bubble Tea apps today means either:

  • Manual testing (slow, error-prone, not CI-friendly)
  • Writing brittle tests that call Update() manually and inspect model internals
  • No snapshot testing, no input simulation, no standard assertions

The Solution

func TestMyApp(t *testing.T) {
    sim := charmtest.New(NewMyModel())

    sim.Type("hello world")        // simulate typing
    sim.SendKey("enter")           // simulate key press

    charmtest.RequireViewContains(t, sim, "hello world")  // assert on output
    charmtest.RequireSnapshot(t, sim)                      // snapshot testing
}

Installation

go get github.com/junhinhow/charm-test

Features

Simulator

The Simulator drives a Bubble Tea model synchronously — no goroutines, no terminal, fully deterministic.

sim := charmtest.New(myModel)
sim := charmtest.New(myModel, charmtest.WithSize(120, 40))
Input Simulation
sim.SendKey("enter")              // named keys
sim.SendKey("j")                  // single characters
sim.SendKey("ctrl+c")             // key combinations
sim.SendKeys("k", "k", "k")      // multiple keys
sim.Type("hello")                 // type a string (char by char)
sim.Resize(100, 30)               // simulate terminal resize

Supported keys: enter, esc, tab, shift+tab, backspace, delete, up, down, left, right, home, end, pgup, pgdown, space, ctrl+c, ctrl+a, ctrl+d, ctrl+e, ctrl+k, ctrl+u, ctrl+n, ctrl+p, and any single character.

Assertions
charmtest.RequireView(t, sim, "exact match")
charmtest.RequireViewContains(t, sim, "substring")
charmtest.RequireViewNotContains(t, sim, "should not appear")
charmtest.RequireViewLines(t, sim, 5)  // exactly 5 non-empty lines

All assertions strip ANSI escape sequences before comparison, so styled output is matched by content only.

Snapshot Testing
charmtest.RequireSnapshot(t, sim)                       // auto-named by test
charmtest.RequireSnapshotNamed(t, sim, "after-login")   // custom name

Golden files are stored in testdata/. On first run, the snapshot is created. On subsequent runs, the view is compared against the stored snapshot.

Update snapshots: CHARM_TEST_UPDATE=1 go test ./...

Debugging
charmtest.DumpView(t, sim)        // print current view to test log
charmtest.DumpMessages(t, sim)    // print all processed messages
charmtest.ViewString(sim)         // get ANSI-stripped view as string
Model State Access
model := sim.Model().(*MyModel)   // type assert to your model
assert.Equal(t, 5, model.Count)

Full Example

See examples/counter/ for a complete working example with tests.

func TestCounter_Increment(t *testing.T) {
    sim := charmtest.New(counter.New())

    sim.SendKeys("k", "k", "k")
    charmtest.RequireViewContains(t, sim, "Count: 3")
}

func TestCounter_Snapshot(t *testing.T) {
    sim := charmtest.New(counter.New())
    sim.SendKeys("k", "k", "k", "k", "k")
    charmtest.RequireSnapshot(t, sim)
}

Design Principles

  • Deterministic: No goroutines, no timers — same input always produces same output
  • Fast: Tests run in microseconds, not seconds
  • Simple: One import, one constructor, intuitive API
  • Idiomatic: Works with testing.T, follows Go testing conventions
  • ANSI-aware: All comparisons strip escape sequences automatically

License

MIT

Documentation

Overview

Package charmtest provides a testing framework for Bubble Tea applications.

It simulates the Bubble Tea runtime without requiring a real terminal, allowing you to send messages, simulate key presses, and assert on the rendered output and model state.

Basic usage:

func TestMyModel(t *testing.T) {
    m := NewMyModel()
    sim := charmtest.New(m)
    sim.SendKey("j")           // simulate pressing "j"
    sim.SendKey("enter")       // simulate pressing enter
    charmtest.RequireView(t, sim, "expected output")
}

Index

Constants

View Source
const DefaultHeight = 24

DefaultHeight is the default terminal height for simulations.

View Source
const DefaultWidth = 80

DefaultWidth is the default terminal width for simulations.

Variables

This section is empty.

Functions

func DumpMessages

func DumpMessages(t *testing.T, s *Simulator)

DumpMessages prints all messages processed by the simulator to the test log for debugging.

func DumpView

func DumpView(t *testing.T, s *Simulator)

DumpView prints the current view (ANSI-stripped) to the test log for debugging purposes.

func RequireSnapshot

func RequireSnapshot(t *testing.T, s *Simulator)

RequireSnapshot compares the current view against a golden file stored in testdata/<testname>.golden. If the golden file does not exist, it is created automatically (the test passes on first run, then validates on subsequent runs).

Set the CHARM_TEST_UPDATE=1 environment variable to overwrite existing golden files when the expected output changes intentionally.

Usage:

func TestMyView(t *testing.T) {
    sim := charmtest.New(NewMyModel())
    sim.SendKey("j")
    charmtest.RequireSnapshot(t, sim)
}

func RequireSnapshotNamed

func RequireSnapshotNamed(t *testing.T, s *Simulator, name string)

RequireSnapshotNamed is like RequireSnapshot but uses a custom name for the golden file instead of the test name. Useful when a single test captures multiple snapshots at different points.

func RequireView

func RequireView(t *testing.T, s *Simulator, expected string)

RequireView asserts that the simulator's current view matches the expected string exactly. ANSI escape sequences are stripped before comparison.

func RequireViewContains

func RequireViewContains(t *testing.T, s *Simulator, substr string)

RequireViewContains asserts that the stripped view contains the given substring.

func RequireViewLines

func RequireViewLines(t *testing.T, s *Simulator, expected int)

RequireViewLines asserts that the view has exactly the expected number of non-empty lines.

func RequireViewNotContains

func RequireViewNotContains(t *testing.T, s *Simulator, substr string)

RequireViewNotContains asserts that the stripped view does NOT contain the given substring.

func ViewString

func ViewString(s *Simulator) string

ViewString returns the current view with ANSI sequences stripped, useful for debugging in tests.

Types

type Option

type Option func(*Simulator)

Option configures a Simulator.

func WithSize

func WithSize(width, height int) Option

WithSize sets the terminal dimensions for the simulation.

type Simulator

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

Simulator drives a Bubble Tea model through its lifecycle without a real terminal. It processes messages synchronously, making tests deterministic.

func New

func New(model tea.Model, opts ...Option) *Simulator

New creates a Simulator that wraps the given Bubble Tea model. The model's Init() is called and resulting commands are processed.

func (*Simulator) Messages

func (s *Simulator) Messages() []tea.Msg

Messages returns all messages that were sent to the model.

func (*Simulator) Model

func (s *Simulator) Model() tea.Model

Model returns the current model state. Use type assertion to access your specific model type:

m := sim.Model().(*MyModel)

func (*Simulator) Resize

func (s *Simulator) Resize(width, height int)

Resize simulates a terminal resize event.

func (*Simulator) Send

func (s *Simulator) Send(msg tea.Msg)

Send sends a message to the model and processes the resulting command.

func (*Simulator) SendKey

func (s *Simulator) SendKey(key string)

SendKey simulates a key press. Accepts key names like "enter", "esc", "tab", "up", "down", "ctrl+c", or single characters like "a", "j".

func (*Simulator) SendKeys

func (s *Simulator) SendKeys(keys ...string)

SendKeys simulates a sequence of key presses.

func (*Simulator) Type

func (s *Simulator) Type(text string)

Type simulates typing a string, sending one key press per character.

func (*Simulator) View

func (s *Simulator) View() string

View returns the current rendered output of the model.

func (*Simulator) ViewContains

func (s *Simulator) ViewContains(substr string) bool

ViewContains returns true if the current view contains the given substring.

Directories

Path Synopsis
examples
counter command
Example: a simple counter model for demonstrating charm-test.
Example: a simple counter model for demonstrating charm-test.

Jump to

Keyboard shortcuts

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