Documentation
¶
Overview ¶
Package btest accessibility helpers.
This file implements WCAG 2.1 contrast ratio checking for terminal color pairs. It converts ANSI 256-color and true-color values to relative luminance and computes contrast ratios per the W3C formula.
References:
- https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio
- https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
Package btest provides a test toolkit for TUI applications built with Bubble Tea. It offers a virtual terminal, assertion helpers, snapshot testing, session recording, fuzz testing, mutation testing, and accessibility checks — everything needed to thoroughly test interactive terminal programs without a real TTY.
Quick Start ¶
Create a TestModel to drive your tea.Model synchronously:
tm := btest.NewTestModel(t, myModel, 80, 24)
tm.SendKey("enter")
btest.AssertContains(t, tm.Screen(), "Welcome")
Core Concepts ¶
- TestModel wraps a tea.Model and processes messages synchronously. Use SendKey, Type, SendResize, and SendMsg to drive the model.
- Screen is a virtual terminal that decodes ANSI output into a grid of cells with text and style attributes.
- Assertion functions (AssertContains, AssertRowEquals, AssertStyleAt, etc.) check screen content and fail the test with descriptive messages.
Snapshot Testing ¶
AssertSnapshot and AssertGolden compare screen content against stored files. On first run (or with -btest.update), the file is created. On subsequent runs, mismatches fail the test with a diff.
btest.AssertSnapshot(t, tm.Screen(), "login-form")
Session Recording ¶
SessionRecorder captures a sequence of inputs and screens that can be saved to .tuisess files and replayed with Replay for regression testing.
rec := btest.NewSessionRecorder(tm)
rec.Key("down").Key("enter").Type("hello")
rec.Save("testdata/sessions/flow.tuisess")
Fuzz Testing ¶
Fuzz and FuzzAndFail send random inputs to a model to find panics. Configure weights for different event types via FuzzConfig.
btest.FuzzAndFail(t, myModel, btest.FuzzConfig{
Iterations: 1000,
Seed: 42,
})
Mutation Testing ¶
MutationTest verifies test suite quality by injecting behavioral mutations into a model and checking that tests detect them.
report := btest.MutationTest(t, factory, btest.MutationConfig{
Test: func(t testing.TB, m tea.Model) { /* assertions */ },
})
btest.AssertMutationScore(t, report, 80.0)
Accessibility ¶
Contrast checking (CheckContrast, AssertContrast), keyboard navigation verification (CheckKeyNav, AssertKeyNavCycle), and color blindness simulation (SimulateColorBlind, AssertDistinguishable) help ensure your TUI is accessible.
Smoke Testing ¶
SmokeTest runs a battery of automated checks (init/render, key handling, resize, mouse) against any tea.Model to catch common issues.
btest.SmokeTest(t, myModel, nil)
Harness ¶
Harness provides a fluent API for concise test scripts:
btest.NewHarness(t, model, 80, 24).
Keys("down", "down", "enter").
Expect("Selected").
Done()
Package btest mutation testing for TUI models.
Mutation testing verifies the quality of a test suite by injecting small behavioral changes ("mutations") into a tea.Model and checking whether the tests catch them. A mutation that goes undetected indicates a gap in the test suite.
Usage:
result := btest.MutationTest(t, func() tea.Model { return NewMyModel() }, btest.MutationConfig{
Test: func(t testing.TB, model tea.Model) {
tm := btest.NewTestModel(t, model, 80, 24)
tm.SendKey("enter")
btest.AssertContains(t, tm.Screen(), "expected")
},
})
// result.Killed / result.Survived / result.Total
Package btest provides a virtual terminal screen and assertion helpers for testing Bubble Tea TUI applications. It uses go-te to emulate a terminal and parse ANSI escape sequences, enabling screen-based assertions on rendered View() output.
Package btest session record/replay.
A .tuisess file captures a deterministic sequence of input events against a tea.Model and the resulting screen after each step. Consumer apps can commit these files under `testdata/sessions/` as end-to-end regression fixtures; running Replay against the same model should yield identical screens at every step.
Format (JSON):
{
"version": 1,
"cols": 80,
"lines": 24,
"steps": [
{"kind": "key", "key": "down"},
{"kind": "screen", "screen": "..."},
{"kind": "type", "text": "abc"},
{"kind": "screen", "screen": "..."}
]
}
The "screen" steps are the expected screen strings captured right after the previous input step. Replay compares the live screen to the expected screen and fails the test on any divergence.
Index ¶
- Constants
- Variables
- func AssertAllDistinguishable(t testing.TB, colors []RGB, typ ColorBlindType)
- func AssertBgAt(t testing.TB, s *Screen, row, col int, color string)
- func AssertBoldAt(t testing.TB, s *Screen, row, col int)
- func AssertColumnContains(t testing.TB, s *Screen, col, startRow, endRow int, text string)
- func AssertColumnCount(t testing.TB, s *Screen, col, startRow, endRow int, text string, want int)
- func AssertContains(t testing.TB, s *Screen, text string)
- func AssertContainsAt(t testing.TB, s *Screen, row, col int, text string)
- func AssertContainsCount(t testing.TB, s *Screen, text string, n int)
- func AssertContrast(t testing.TB, fg, bg RGB, level WCAGLevel)
- func AssertContrastLarge(t testing.TB, fg, bg RGB, level WCAGLevel)
- func AssertCursorAt(t testing.TB, s *Screen, row, col int)
- func AssertCursorRowContains(t testing.TB, s *Screen, text string)
- func AssertDistinguishable(t testing.TB, a, b RGB, typ ColorBlindType)
- func AssertEmpty(t testing.TB, s *Screen)
- func AssertFgAt(t testing.TB, s *Screen, row, col int, color string)
- func AssertGolden(t testing.TB, s *Screen, name string)
- func AssertItalicAt(t testing.TB, s *Screen, row, col int)
- func AssertKeyNavCycle(t testing.TB, model tea.Model, minStops int, cfg KeyNavConfig)
- func AssertKeyNavRoundTrip(t testing.TB, model tea.Model, cfg KeyNavConfig)
- func AssertKeybind(t testing.TB, s *Screen, key, description string)
- func AssertMatches(t testing.TB, s *Screen, pattern string)
- func AssertMutationScore(t testing.TB, report *MutationReport, min float64)
- func AssertNoANSI(t testing.TB, s *Screen)
- func AssertNotContains(t testing.TB, s *Screen, text string)
- func AssertNotEmpty(t testing.TB, s *Screen)
- func AssertRegionBg(t testing.TB, s *Screen, row, col, width, height int, color string)
- func AssertRegionBold(t testing.TB, s *Screen, row, col, width, height int)
- func AssertRegionContains(t testing.TB, s *Screen, row, col, width, height int, text string)
- func AssertRegionFg(t testing.TB, s *Screen, row, col, width, height int, color string)
- func AssertRegionNotContains(t testing.TB, s *Screen, row, col, width, height int, text string)
- func AssertReverseAt(t testing.TB, s *Screen, row, col int)
- func AssertRowContains(t testing.TB, s *Screen, row int, text string)
- func AssertRowCount(t testing.TB, s *Screen, want int)
- func AssertRowEquals(t testing.TB, s *Screen, row int, text string)
- func AssertRowMatches(t testing.TB, s *Screen, row int, pattern string)
- func AssertRowNotContains(t testing.TB, s *Screen, row int, text string)
- func AssertScreenEquals(t testing.TB, s *Screen, expected string)
- func AssertScreenMatches(t testing.TB, s *Screen, pattern string)
- func AssertScreensEqual(t testing.TB, a, b *Screen)
- func AssertScreensNotEqual(t testing.TB, a, b *Screen)
- func AssertSnapshot(t testing.TB, scr *Screen, name string)
- func AssertStyleAt(t testing.TB, s *Screen, row, col int, style CellStyle)
- func AssertTextAt(t testing.TB, s *Screen, row, col int, text string)
- func AssertUnderlineAt(t testing.TB, s *Screen, row, col int)
- func CheckKeyNavRoundTrip(t testing.TB, model tea.Model, cfg KeyNavConfig) bool
- func ColorDistance(a, b RGB) float64
- func ColorPairDistinguishable(a, b RGB, typ ColorBlindType, threshold float64) bool
- func ContrastRatio(fg, bg RGB) float64
- func FormatSequence(events []FuzzEvent) string
- func FuzzAndFail(t testing.TB, model tea.Model, cfg FuzzConfig)
- func HTMLReporter(t testing.TB, report *Report, path string)
- func JUnitReporter(t testing.TB, report *Report, path string)
- func KeyMsgForTesting(key string) tea.KeyMsg
- func KeyNames() []string
- func KeyNavReport(result *KeyNavResult) string
- func ListFailureCaptures() ([]string, error)
- func PendingGoldenPath(goldenPath string) string
- func Replay(t testing.TB, model tea.Model, path string)
- func SaveFailureCapture(t testing.TB, fc FailureCapture)
- func SmokeTest(t testing.TB, model tea.Model, opts *SmokeOpts)
- func SnapshotApp(t testing.TB, app interface{ ... }, cols, lines int, name string)
- func SnapshotPath(name string) string
- func UntilContains(text string) screenPredicate
- func UntilNotContains(text string) screenPredicate
- func UntilRowContains(row int, text string) screenPredicate
- type CellDiff
- type CellKind
- type CellStyle
- type Clock
- type ColorBlindPairResult
- type ColorBlindReport
- type ColorBlindType
- type ContrastReport
- type ContrastResult
- type Diff
- type DiffLine
- type DiffMode
- type DiffViewer
- func (dv *DiffViewer) Focused() bool
- func (dv *DiffViewer) Init() tea.Cmd
- func (dv *DiffViewer) KeyBindings() []interface{}
- func (dv *DiffViewer) Mode() DiffMode
- func (dv *DiffViewer) SetFocused(f bool)
- func (dv *DiffViewer) SetMode(m DiffMode)
- func (dv *DiffViewer) SetSize(w, h int)
- func (dv *DiffViewer) Update(msg tea.Msg, _ interface{}) (*DiffViewer, tea.Cmd)
- func (dv *DiffViewer) View() string
- type DiffViewerBackMsg
- type FailureCapture
- type FailureKind
- type FakeClock
- type FakeTimer
- type FuzzConfig
- type FuzzEvent
- type FuzzResult
- type Harness
- func (h *Harness) Advance(d time.Duration) *Harness
- func (h *Harness) Click(x, y int) *Harness
- func (h *Harness) Done()
- func (h *Harness) Expect(text string) *Harness
- func (h *Harness) ExpectNot(text string) *Harness
- func (h *Harness) ExpectRow(row int, text string) *Harness
- func (h *Harness) Keys(keys ...string) *Harness
- func (h *Harness) OnSetup(fn func()) *Harness
- func (h *Harness) OnTeardown(fn func()) *Harness
- func (h *Harness) Resize(cols, lines int) *Harness
- func (h *Harness) Screen() *Screen
- func (h *Harness) Send(msg tea.Msg) *Harness
- func (h *Harness) Snapshot(name string) *Harness
- func (h *Harness) TestModel() *TestModel
- func (h *Harness) Type(text string) *Harness
- type IndexedRow
- type KeyNavConfig
- type KeyNavResult
- type Mutation
- type MutationConfig
- type MutationReport
- type MutationResult
- type MutationType
- type PendingGolden
- type RGB
- type RealClock
- type Region
- func (r *Region) AllRows() []string
- func (r *Region) Contains(text string) bool
- func (r *Region) CountOccurrences(text string) int
- func (r *Region) FindText(text string) (row, col int)
- func (r *Region) IsEmpty() bool
- func (r *Region) Row(row int) string
- func (r *Region) RowCount() int
- func (r *Region) String() string
- func (r *Region) StyleAt(row, col int) CellStyle
- type Report
- type Screen
- func (s *Screen) AllRows() []string
- func (s *Screen) Column(col, startRow, endRow int) string
- func (s *Screen) Contains(text string) bool
- func (s *Screen) ContainsAt(row, col int, text string) bool
- func (s *Screen) CountOccurrences(text string) int
- func (s *Screen) CursorPos() (row, col int)
- func (s *Screen) FindAllText(text string) [][2]int
- func (s *Screen) FindRegexp(pattern string) (row, col int)
- func (s *Screen) FindText(text string) (row, col int)
- func (s *Screen) IsEmpty() bool
- func (s *Screen) MatchesRegexp(pattern string) bool
- func (s *Screen) NonEmptyRows() []IndexedRow
- func (s *Screen) Region(row, col, width, height int) *Region
- func (s *Screen) Render(output string)
- func (s *Screen) Row(row int) string
- func (s *Screen) RowCount() int
- func (s *Screen) Size() (cols, lines int)
- func (s *Screen) String() string
- func (s *Screen) StyleAt(row, col int) CellStyle
- func (s *Screen) TextAt(row, startCol, endCol int) string
- type Session
- type SessionRecorder
- type SessionStep
- type SmokeOpts
- type SmokeReport
- type SmokeResult
- type SmokeSize
- type Stopwatch
- type TestModel
- func (tm *TestModel) AdvanceClock(d time.Duration)
- func (tm *TestModel) Clock() *FakeClock
- func (tm *TestModel) Cols() int
- func (tm *TestModel) Lines() int
- func (tm *TestModel) Model() tea.Model
- func (tm *TestModel) RequireScreen(fn func(t testing.TB, s *Screen))
- func (tm *TestModel) Screen() *Screen
- func (tm *TestModel) SendKey(key string)
- func (tm *TestModel) SendKeys(keys ...string)
- func (tm *TestModel) SendMouse(x, y int, button tea.MouseButton)
- func (tm *TestModel) SendMsg(msg tea.Msg)
- func (tm *TestModel) SendResize(cols, lines int)
- func (tm *TestModel) SendTick()deprecated
- func (tm *TestModel) TriggerTick()
- func (tm *TestModel) Type(text string)
- func (tm *TestModel) WaitFor(predicate func(*Screen) bool, maxTicks int) bool
- func (tm *TestModel) WithClock(clock *FakeClock) *TestModel
- type TestResult
- type TickMsg
- type TickMsgPlaceholder
- type WCAGLevel
Examples ¶
Constants ¶
const SessionFormatVersion = 2
SessionFormatVersion is the on-disk version marker. Bump whenever the step schema changes in a backwards-incompatible way.
History:
v1 – initial format (blit ≤ v0.7.1) v2 – adds Name, RecordedAt, Command metadata fields (blit v0.11+)
Variables ¶
var SnapshotDir = filepath.Join("testdata", "__snapshots__")
SnapshotDir is the subdirectory (relative to the test file's package) where snapshot files are written. Kept exported so users can override it for a specific package if they dislike the default.
Functions ¶
func AssertAllDistinguishable ¶ added in v0.2.12
func AssertAllDistinguishable(t testing.TB, colors []RGB, typ ColorBlindType)
AssertAllDistinguishable fails the test if any pair of colors in the palette is not distinguishable under the specified color blindness type.
func AssertBgAt ¶
AssertBgAt fails if the background color at (row, col) doesn't match.
func AssertBoldAt ¶
AssertBoldAt fails the test if the cell at (row, col) is not bold.
func AssertColumnContains ¶
AssertColumnContains asserts any row in column range [startRow,endRow] contains text.
func AssertColumnCount ¶
AssertColumnCount asserts how many non-empty rows contain text in the given column range.
func AssertContains ¶
AssertContains fails the test if the screen doesn't contain the text.
Example ¶
package main
import (
"fmt"
"testing"
"github.com/blitui/blit/btest"
tea "github.com/charmbracelet/bubbletea"
)
// listModel is a minimal model used in examples.
type listModel struct {
items []string
cursor int
chosen string
}
func newListModel() *listModel {
return &listModel{items: []string{"Alpha", "Beta", "Gamma"}}
}
func (m *listModel) Init() tea.Cmd { return nil }
func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if km, ok := msg.(tea.KeyMsg); ok {
switch km.Type {
case tea.KeyDown:
if m.cursor < len(m.items)-1 {
m.cursor++
}
case tea.KeyUp:
if m.cursor > 0 {
m.cursor--
}
case tea.KeyEnter:
m.chosen = m.items[m.cursor]
}
}
return m, nil
}
func (m *listModel) View() string {
s := ""
for i, item := range m.items {
cursor := " "
if i == m.cursor {
cursor = "> "
}
s += cursor + item + "\n"
}
if m.chosen != "" {
s += "\nSelected: " + m.chosen
}
return s
}
func main() {
t := &testing.T{}
model := newListModel()
tm := btest.NewTestModel(t, model, 40, 10)
// AssertContains checks that the screen contains the given text.
btest.AssertContains(t, tm.Screen(), "Alpha")
btest.AssertContains(t, tm.Screen(), "Beta")
btest.AssertContains(t, tm.Screen(), "Gamma")
fmt.Println("all items visible")
}
Output: all items visible
func AssertContainsAt ¶
AssertContainsAt fails the test if text doesn't appear at (row, col).
func AssertContainsCount ¶
AssertContainsCount fails if text doesn't appear exactly n times on screen.
func AssertContrast ¶ added in v0.2.12
AssertContrast fails the test if the contrast ratio between fg and bg does not meet the specified WCAG level for normal text.
func AssertContrastLarge ¶ added in v0.2.12
AssertContrastLarge fails the test if the contrast ratio between fg and bg does not meet the specified WCAG level for large text.
func AssertCursorAt ¶
AssertCursorAt fails the test if the cursor isn't at (row, col).
func AssertCursorRowContains ¶
AssertCursorRowContains asserts the row under the cursor contains text.
func AssertDistinguishable ¶ added in v0.2.12
func AssertDistinguishable(t testing.TB, a, b RGB, typ ColorBlindType)
AssertDistinguishable fails the test if the two colors are not distinguishable under the specified color blindness type.
func AssertEmpty ¶
AssertEmpty fails if the screen has any visible content.
func AssertFgAt ¶
AssertFgAt fails if the foreground color at (row, col) doesn't match.
func AssertGolden ¶
AssertGolden compares the screen content against a golden file. If the file doesn't exist or -update flag is set, it creates/updates the golden file. Golden files are stored in testdata/ relative to the test file.
func AssertItalicAt ¶
AssertItalicAt fails if the cell at (row, col) is not italic.
func AssertKeyNavCycle ¶ added in v0.2.12
AssertKeyNavCycle fails the test if the model does not have a keyboard navigation cycle (at least minStops distinct focus states reachable via tab).
func AssertKeyNavRoundTrip ¶ added in v0.2.12
func AssertKeyNavRoundTrip(t testing.TB, model tea.Model, cfg KeyNavConfig)
AssertKeyNavRoundTrip fails the test if tabbing forward and then backward does not return to the initial state.
func AssertKeybind ¶
AssertKeybind asserts the screen's footer/help line contains the given key label. It searches the whole screen for a "key description" pattern anywhere.
func AssertMatches ¶
AssertMatches fails if the screen content doesn't match the regular expression.
func AssertMutationScore ¶ added in v0.2.12
func AssertMutationScore(t testing.TB, report *MutationReport, min float64)
AssertMutationScore fails the test if the mutation score is below min.
func AssertNoANSI ¶
AssertNoANSI asserts the screen's rendered text contains no raw ANSI escape sequences. Raw ANSI in the decoded virtual screen indicates a broken writer that emitted literal bytes instead of styled runs.
func AssertNotContains ¶
AssertNotContains fails the test if the screen contains the text.
func AssertNotEmpty ¶
AssertNotEmpty fails if the screen has no visible content.
func AssertRegionBg ¶
AssertRegionBg asserts every non-space cell in a region has the given bg color.
func AssertRegionBold ¶
AssertRegionBold asserts every non-space cell in a region is bold.
func AssertRegionContains ¶
AssertRegionContains fails if the region doesn't contain the text.
func AssertRegionFg ¶
AssertRegionFg asserts every non-space cell in a region has the given fg color.
func AssertRegionNotContains ¶
AssertRegionNotContains fails if the region contains the text.
func AssertReverseAt ¶
AssertReverseAt fails if the cell at (row, col) is not reversed.
func AssertRowContains ¶
AssertRowContains fails if the given row doesn't contain the text.
Example ¶
package main
import (
"fmt"
"testing"
"github.com/blitui/blit/btest"
tea "github.com/charmbracelet/bubbletea"
)
// listModel is a minimal model used in examples.
type listModel struct {
items []string
cursor int
chosen string
}
func newListModel() *listModel {
return &listModel{items: []string{"Alpha", "Beta", "Gamma"}}
}
func (m *listModel) Init() tea.Cmd { return nil }
func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if km, ok := msg.(tea.KeyMsg); ok {
switch km.Type {
case tea.KeyDown:
if m.cursor < len(m.items)-1 {
m.cursor++
}
case tea.KeyUp:
if m.cursor > 0 {
m.cursor--
}
case tea.KeyEnter:
m.chosen = m.items[m.cursor]
}
}
return m, nil
}
func (m *listModel) View() string {
s := ""
for i, item := range m.items {
cursor := " "
if i == m.cursor {
cursor = "> "
}
s += cursor + item + "\n"
}
if m.chosen != "" {
s += "\nSelected: " + m.chosen
}
return s
}
func main() {
t := &testing.T{}
model := newListModel()
tm := btest.NewTestModel(t, model, 40, 10)
// First row should have the cursor on Alpha.
btest.AssertRowContains(t, tm.Screen(), 0, "> Alpha")
fmt.Println("cursor on Alpha")
}
Output: cursor on Alpha
func AssertRowCount ¶
AssertRowCount fails if the number of non-empty rows doesn't match.
func AssertRowEquals ¶
AssertRowEquals fails if the given row doesn't exactly equal text (trimmed).
func AssertRowMatches ¶
AssertRowMatches fails if the given row doesn't match the regular expression.
func AssertRowNotContains ¶
AssertRowNotContains fails if the given row contains the text.
func AssertScreenEquals ¶
AssertScreenEquals fails if the full screen text doesn't exactly match expected. On failure it also persists a FailureCapture so `blit diff` can replay it.
func AssertScreenMatches ¶
AssertScreenMatches is a regexp variant of AssertScreenEquals for fuzzier matches.
func AssertScreensEqual ¶
AssertScreensEqual fails if two screens don't have identical text content. On failure it also persists a FailureCapture so `blit diff` can replay it.
func AssertScreensNotEqual ¶
AssertScreensNotEqual fails if two screens have identical text content.
func AssertSnapshot ¶
AssertSnapshot compares scr's current screen contents against a previously stored snapshot named <name>.snap under SnapshotDir. On first run (or when -btest.update is passed), the snapshot is (re)generated. Line endings are normalized to \n so snapshots round-trip across OSes.
Example:
scr := tm.Screen() btest.AssertSnapshot(t, scr, "login-form")
Example ¶
package main
import (
"fmt"
)
func main() {
// AssertSnapshot compares screen content against a stored .snap file.
// On first run, the snapshot is created. On subsequent runs, mismatches
// cause the test to fail.
//
// Usage:
// tm := btest.NewTestModel(t, model, 80, 24)
// btest.AssertSnapshot(t, tm.Screen(), "my-component")
//
// Regenerate snapshots:
// go test ./... -args -btest.update
fmt.Println("see usage in doc comment")
}
Output: see usage in doc comment
func AssertStyleAt ¶
AssertStyleAt fails the test if the cell at (row, col) doesn't match the style.
func AssertTextAt ¶
AssertTextAt fails if the text at (row, col) through (row, col+len) doesn't match.
func AssertUnderlineAt ¶
AssertUnderlineAt fails if the cell at (row, col) is not underlined.
func CheckKeyNavRoundTrip ¶ added in v0.2.12
CheckKeyNavRoundTrip verifies that pressing tab N times and then shift+tab N times returns to the original screen state.
func ColorDistance ¶ added in v0.2.12
ColorDistance computes the Euclidean distance between two colors in linear RGB space. Values range from 0 (identical) to ~1.73 (max).
func ColorPairDistinguishable ¶ added in v0.2.12
func ColorPairDistinguishable(a, b RGB, typ ColorBlindType, threshold float64) bool
ColorPairDistinguishable checks whether two colors remain visually distinct under the given color blindness simulation. The threshold is the minimum color distance required (0.1 is a reasonable default).
func ContrastRatio ¶ added in v0.2.12
ContrastRatio computes the WCAG 2.1 contrast ratio between two colors. The result is in the range [1.0, 21.0].
Example ¶
package main
import (
"fmt"
"github.com/blitui/blit/btest"
)
func main() {
// Check WCAG contrast ratio between two colors.
black := btest.RGB{R: 0, G: 0, B: 0}
white := btest.RGB{R: 255, G: 255, B: 255}
ratio := btest.ContrastRatio(black, white)
fmt.Printf("contrast ratio: %.1f:1\n", ratio)
}
Output: contrast ratio: 21.0:1
func FormatSequence ¶ added in v0.2.12
FormatSequence returns a human-readable string of a fuzz event sequence.
func FuzzAndFail ¶ added in v0.2.12
func FuzzAndFail(t testing.TB, model tea.Model, cfg FuzzConfig)
FuzzAndFail runs Fuzz and fails the test if the model panics, printing the failing sequence for reproduction.
func HTMLReporter ¶
HTMLReporter registers a t.Cleanup hook that writes the report as HTML to path when the test finishes.
func JUnitReporter ¶
JUnitReporter registers a t.Cleanup hook that writes the report as JUnit XML to path when the test (or parent TestMain) finishes. The caller owns the Report and is responsible for populating Results before teardown.
func KeyMsgForTesting ¶
KeyMsgForTesting converts a key name to a tea.KeyMsg for use in unit tests that call Component.Update directly without a full TestModel.
func KeyNames ¶
func KeyNames() []string
KeyNames returns the list of all recognized key names for documentation.
func KeyNavReport ¶ added in v0.2.12
func KeyNavReport(result *KeyNavResult) string
KeyNavReport generates a human-readable summary of keyboard navigation.
func ListFailureCaptures ¶
ListFailureCaptures returns test names for all persisted failure captures.
func PendingGoldenPath ¶
PendingGoldenPath returns the path of the pending-review file for a golden. The pending file is <goldenPath>.new and is written by AssertGolden on mismatch so that `blit review` can enumerate it.
func Replay ¶
Replay drives the given model through a recorded session, asserting that each screen step matches the live screen. It fails the test on any divergence. Cols/lines from the session override the model's initial size.
func SaveFailureCapture ¶
func SaveFailureCapture(t testing.TB, fc FailureCapture)
SaveFailureCapture persists fc to .blit/failures/<testname>.json. If fc.TestName is empty and t implements Name(), the test name is filled in. Errors are logged via t but never fatal — failure capture is best-effort.
func SmokeTest ¶ added in v0.1.23
SmokeTest is a convenience wrapper that calls Smoke and fails the test if any check fails.
Example ¶
package main
import (
"fmt"
"testing"
"github.com/blitui/blit/btest"
tea "github.com/charmbracelet/bubbletea"
)
// listModel is a minimal model used in examples.
type listModel struct {
items []string
cursor int
chosen string
}
func newListModel() *listModel {
return &listModel{items: []string{"Alpha", "Beta", "Gamma"}}
}
func (m *listModel) Init() tea.Cmd { return nil }
func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if km, ok := msg.(tea.KeyMsg); ok {
switch km.Type {
case tea.KeyDown:
if m.cursor < len(m.items)-1 {
m.cursor++
}
case tea.KeyUp:
if m.cursor > 0 {
m.cursor--
}
case tea.KeyEnter:
m.chosen = m.items[m.cursor]
}
}
return m, nil
}
func (m *listModel) View() string {
s := ""
for i, item := range m.items {
cursor := " "
if i == m.cursor {
cursor = "> "
}
s += cursor + item + "\n"
}
if m.chosen != "" {
s += "\nSelected: " + m.chosen
}
return s
}
func main() {
t := &testing.T{}
model := newListModel()
// SmokeTest runs automated checks against the model.
btest.SmokeTest(t, model, nil)
fmt.Println("smoke test passed")
}
Output: smoke test passed
func SnapshotApp ¶ added in v0.1.2
SnapshotApp is a one-liner that creates a harness from a full application, takes a named golden snapshot, and cleans up. Use this when you only need to verify the initial render of the real app layout:
btest.SnapshotApp(t, myApp, 80, 24, "initial")
For interactive tests (key presses, resizes, multiple snapshots), use NewAppHarness instead.
func SnapshotPath ¶
SnapshotPath returns the full on-disk path for the named snapshot. Exposed so tests can assert on where a snapshot landed.
func UntilContains ¶
func UntilContains(text string) screenPredicate
UntilContains returns a predicate that is satisfied when the screen contains text.
func UntilNotContains ¶
func UntilNotContains(text string) screenPredicate
UntilNotContains returns a predicate satisfied when the screen no longer contains text.
func UntilRowContains ¶
UntilRowContains returns a predicate satisfied when the given row contains text.
Types ¶
type CellDiff ¶
type CellDiff struct {
Row int
Col int
ExpectedText string
ActualText string
ExpectedStyle CellStyle
ActualStyle CellStyle
Kind CellKind
}
CellDiff holds the comparison result for a single terminal cell.
func ScreenCellDiff ¶
ScreenCellDiff performs a per-cell comparison between two screens, returning one CellDiff per cell that differs (text or style).
type CellKind ¶
type CellKind int
CellKind classifies the type of difference at a single cell position.
type CellStyle ¶
type CellStyle struct {
Fg string // foreground color (empty = default)
Bg string // background color (empty = default)
Bold bool
Italic bool
Underline bool
Reverse bool
}
CellStyle represents the visual attributes of a terminal cell.
type Clock ¶
type Clock interface {
// Now returns the current time as seen by this clock.
Now() time.Time
// Sleep blocks until the clock has advanced by d.
// FakeClock implementations return immediately but still honor Advance.
Sleep(d time.Duration)
}
Clock is an abstraction over time.Now used by Poller-like components so tests can drive time deterministically. The real clock uses time.Now and time.Sleep; FakeClock lets tests advance time manually.
type ColorBlindPairResult ¶ added in v0.2.12
type ColorBlindPairResult struct {
A, B RGB
Type ColorBlindType
SimA RGB
SimB RGB
Distance float64
Threshold float64
Pass bool
}
ColorBlindPairResult holds the result for a single color pair check.
type ColorBlindReport ¶ added in v0.2.12
type ColorBlindReport struct {
Pairs []ColorBlindPairResult
}
ColorBlindReport holds the results of checking a palette against all three types of color vision deficiency.
func CheckPalette ¶ added in v0.2.12
func CheckPalette(colors []RGB, threshold float64) *ColorBlindReport
CheckPalette tests all pairs of colors against all three color blindness types and returns a detailed report.
func (*ColorBlindReport) Summary ¶ added in v0.2.12
func (r *ColorBlindReport) Summary() string
Summary returns a human-readable summary.
func (*ColorBlindReport) Violations ¶ added in v0.2.12
func (r *ColorBlindReport) Violations() []ColorBlindPairResult
Violations returns only the failing pair results.
type ColorBlindType ¶ added in v0.2.12
type ColorBlindType int
ColorBlindType represents a type of color vision deficiency.
const ( // Protanopia is red-blind (missing L-cones). Protanopia ColorBlindType = iota // Deuteranopia is green-blind (missing M-cones). Deuteranopia // Tritanopia is blue-blind (missing S-cones). Tritanopia )
func (ColorBlindType) String ¶ added in v0.2.12
func (t ColorBlindType) String() string
String returns the name of the color blindness type.
type ContrastReport ¶ added in v0.2.12
type ContrastReport struct {
Results []ContrastResult
}
ContrastReport holds results of checking multiple color pairs.
func (*ContrastReport) Summary ¶ added in v0.2.12
func (r *ContrastReport) Summary(level WCAGLevel) string
Summary returns a human-readable summary of the report.
func (*ContrastReport) Violations ¶ added in v0.2.12
func (r *ContrastReport) Violations(level WCAGLevel) []ContrastResult
Violations returns only the results that fail the specified level for normal text.
type ContrastResult ¶ added in v0.2.12
type ContrastResult struct {
// FG and BG are the foreground and background colors as RGB.
FG, BG RGB
// Ratio is the computed contrast ratio (1.0–21.0).
Ratio float64
// PassAA is true if the ratio meets WCAG AA for normal text (≥ 4.5).
PassAA bool
// PassAAA is true if the ratio meets WCAG AAA for normal text (≥ 7.0).
PassAAA bool
// PassAALarge is true if the ratio meets WCAG AA for large text (≥ 3.0).
PassAALarge bool
// PassAAALarge is true if the ratio meets WCAG AAA for large text (≥ 4.5).
PassAAALarge bool
}
ContrastResult holds the outcome of a contrast ratio check.
func CheckContrast ¶ added in v0.2.12
func CheckContrast(fg, bg RGB) ContrastResult
CheckContrast evaluates the contrast between fg and bg against WCAG criteria.
Example ¶
package main
import (
"fmt"
"github.com/blitui/blit/btest"
)
func main() {
fg := btest.RGB{R: 0, G: 0, B: 0}
bg := btest.RGB{R: 255, G: 255, B: 255}
result := btest.CheckContrast(fg, bg)
fmt.Printf("AA=%v AAA=%v ratio=%.1f\n", result.PassAA, result.PassAAA, result.Ratio)
}
Output: AA=true AAA=true ratio=21.0
type Diff ¶
type Diff struct {
Lines []DiffLine
}
Diff represents a line-by-line comparison between two screen states.
func ScreenDiff ¶
ScreenDiff compares two screens and returns a Diff showing changed lines.
func (Diff) ChangedLines ¶
ChangedLines returns only the lines that differ.
func (Diff) HasChanges ¶
HasChanges reports whether any lines differ.
type DiffViewer ¶
type DiffViewer struct {
// contains filtered or unexported fields
}
DiffViewer is a blit Component that renders a side-by-side (or unified / cells-only) comparison of two screens captured from a failing btest assertion. It implements the blit Component interface so it can be embedded in any App layout.
Keybindings:
s — side-by-side mode u — unified mode d — cells-only mode q — signal back-to-runner (emits DiffViewerBackMsg)
func NewDiffViewer ¶
func NewDiffViewer(fc *FailureCapture) *DiffViewer
NewDiffViewer constructs a DiffViewer from a persisted FailureCapture.
func (*DiffViewer) Focused ¶
func (dv *DiffViewer) Focused() bool
Focused implements blit.Component.
func (*DiffViewer) KeyBindings ¶
func (dv *DiffViewer) KeyBindings() []interface{}
KeyBindings implements blit.Component.
func (*DiffViewer) Mode ¶
func (dv *DiffViewer) Mode() DiffMode
Mode returns the current display mode.
func (*DiffViewer) SetFocused ¶
func (dv *DiffViewer) SetFocused(f bool)
SetFocused implements blit.Component.
func (*DiffViewer) SetMode ¶
func (dv *DiffViewer) SetMode(m DiffMode)
SetMode sets the display mode directly (used by the CLI one-shot renderer).
func (*DiffViewer) SetSize ¶
func (dv *DiffViewer) SetSize(w, h int)
SetSize implements blit.Component.
func (*DiffViewer) Update ¶
func (dv *DiffViewer) Update(msg tea.Msg, _ interface{}) (*DiffViewer, tea.Cmd)
Update implements blit.Component. ctx is the ambient blit.Context but the DiffViewer only needs key messages, so it accepts tea.Msg directly and ignores the ctx value (kept as interface{} to avoid importing blit from inside the btest sub-package, which would create a cycle).
type DiffViewerBackMsg ¶
type DiffViewerBackMsg struct{}
DiffViewerBackMsg is sent when the user presses q to return to the runner.
type FailureCapture ¶
type FailureCapture struct {
TestName string `json:"test_name"`
Kind FailureKind `json:"kind"`
// For FailureScreenEqual: both fields populated.
ExpectedScreen string `json:"expected_screen,omitempty"`
ActualScreen string `json:"actual_screen,omitempty"`
// For FailureGolden: golden file path + expected/actual bytes.
GoldenPath string `json:"golden_path,omitempty"`
GoldenExpected string `json:"golden_expected,omitempty"`
GoldenActual string `json:"golden_actual,omitempty"`
}
FailureCapture holds the screens (or golden bytes) captured when an assertion fails. It is persisted to .blit/failures/<testname>.json so that `blit diff <testname>` can replay the diff offline.
func LoadFailureCapture ¶
func LoadFailureCapture(testName string) (*FailureCapture, error)
LoadFailureCapture reads the persisted capture for a test name.
type FailureKind ¶
type FailureKind string
FailureKind identifies what kind of assertion failed.
const ( // FailureScreenEqual is produced by screen-equality assertions. FailureScreenEqual FailureKind = "screen_equal" // FailureGolden is produced by AssertGolden when the file differs. FailureGolden FailureKind = "golden" )
type FakeClock ¶
type FakeClock struct {
// contains filtered or unexported fields
}
FakeClock is a deterministic Clock for tests. Create one with NewFakeClock and advance it with Advance. Now and Sleep are safe for concurrent use.
func NewFakeClock ¶
NewFakeClock returns a FakeClock anchored at the given time. If t is the zero value, it is anchored at a fixed epoch (2026-01-01 UTC) so tests are reproducible across machines.
func (*FakeClock) Advance ¶
Advance moves the fake clock forward by d and fires any timers whose deadline has been reached. Negative durations are ignored.
func (*FakeClock) AfterFunc ¶ added in v0.2.12
AfterFunc registers a function to be called when the clock advances past the deadline (Now() + d). Returns a Timer that can be stopped. The function fires during the next Advance call that crosses the deadline.
type FakeTimer ¶ added in v0.2.12
type FakeTimer struct {
// contains filtered or unexported fields
}
FakeTimer is a timer registered with FakeClock.AfterFunc.
type FuzzConfig ¶ added in v0.2.12
type FuzzConfig struct {
// Seed for the random number generator. Zero uses a random seed.
Seed int64
// Iterations is the number of random events to generate per run.
// Defaults to 1000.
Iterations int
// Cols and Lines set the initial terminal dimensions.
// Defaults to 80x24.
Cols int
Lines int
// KeyWeight, MouseWeight, ResizeWeight, TickWeight control the
// relative probability of each event type. All default to 1.
KeyWeight int
MouseWeight int
ResizeWeight int
TickWeight int
// MinCols, MaxCols, MinLines, MaxLines constrain random resize events.
// Defaults: 20–200 cols, 5–60 lines.
MinCols int
MaxCols int
MinLines int
MaxLines int
// ExtraKeys are additional key names to include in the random pool
// beyond the built-in keyMap entries.
ExtraKeys []string
}
FuzzConfig configures the random input generator.
type FuzzEvent ¶ added in v0.2.12
type FuzzEvent struct {
Kind string // "key", "mouse", "resize", "tick"
Key string // for Kind="key"
X, Y int // for Kind="mouse"
Button tea.MouseButton
Cols int // for Kind="resize"
Lines int // for Kind="resize"
}
FuzzEvent describes a single random input event generated by the fuzzer.
type FuzzResult ¶ added in v0.2.12
type FuzzResult struct {
// Iterations is the number of events that were applied.
Iterations int
// Panicked is true if the model panicked during the run.
Panicked bool
// PanicValue holds the recovered panic value, if any.
PanicValue interface{}
// PanicEvent is the index of the event that caused the panic.
PanicEvent int
// Events is the full sequence of generated events.
Events []FuzzEvent
}
FuzzResult holds the outcome of a fuzz run.
func Fuzz ¶ added in v0.2.12
func Fuzz(t testing.TB, model tea.Model, cfg FuzzConfig) *FuzzResult
Fuzz runs a random input sequence against a tea.Model and reports whether it panicked. The model is tested inside a TestModel wrapper.
Example ¶
package main
import (
"fmt"
"testing"
"github.com/blitui/blit/btest"
tea "github.com/charmbracelet/bubbletea"
)
// listModel is a minimal model used in examples.
type listModel struct {
items []string
cursor int
chosen string
}
func newListModel() *listModel {
return &listModel{items: []string{"Alpha", "Beta", "Gamma"}}
}
func (m *listModel) Init() tea.Cmd { return nil }
func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if km, ok := msg.(tea.KeyMsg); ok {
switch km.Type {
case tea.KeyDown:
if m.cursor < len(m.items)-1 {
m.cursor++
}
case tea.KeyUp:
if m.cursor > 0 {
m.cursor--
}
case tea.KeyEnter:
m.chosen = m.items[m.cursor]
}
}
return m, nil
}
func (m *listModel) View() string {
s := ""
for i, item := range m.items {
cursor := " "
if i == m.cursor {
cursor = "> "
}
s += cursor + item + "\n"
}
if m.chosen != "" {
s += "\nSelected: " + m.chosen
}
return s
}
func main() {
t := &testing.T{}
model := newListModel()
// Fuzz sends random inputs to find panics.
result := btest.Fuzz(t, model, btest.FuzzConfig{
Seed: 42,
Iterations: 100,
})
fmt.Printf("panicked=%v iterations=%d\n", result.Panicked, result.Iterations)
}
Output: panicked=false iterations=100
func (*FuzzResult) FailingSequence ¶ added in v0.2.12
func (r *FuzzResult) FailingSequence() []FuzzEvent
FailingSequence returns the events up to and including the panic trigger. Returns nil if no panic occurred.
type Harness ¶
type Harness struct {
// contains filtered or unexported fields
}
Harness is a fluent wrapper around TestModel for concise, chainable test scripts. Each method returns *Harness so calls can be chained:
btest.NewHarness(t, model, 80, 24).
Keys("down", "down", "enter").
Expect("Loaded").
ExpectRow(2, "selected").
Done()
func NewAppHarness ¶ added in v0.1.2
NewAppHarness creates a test harness from a full application, using the real component tree (Tabs, DualPane, StatusBar, overlays, etc.). This ensures golden snapshots cover the actual production layout, not a simplified reconstruction. app must implement Model() tea.Model (e.g., *blit.App).
h := btest.NewAppHarness(t, myApp, 80, 24)
defer h.Done()
h.Send(someMsg).Expect("loaded").Snapshot("initial")
func NewHarness ¶
NewHarness creates a new test harness around the given model.
Example ¶
package main
import (
"fmt"
"testing"
"github.com/blitui/blit/btest"
tea "github.com/charmbracelet/bubbletea"
)
// listModel is a minimal model used in examples.
type listModel struct {
items []string
cursor int
chosen string
}
func newListModel() *listModel {
return &listModel{items: []string{"Alpha", "Beta", "Gamma"}}
}
func (m *listModel) Init() tea.Cmd { return nil }
func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if km, ok := msg.(tea.KeyMsg); ok {
switch km.Type {
case tea.KeyDown:
if m.cursor < len(m.items)-1 {
m.cursor++
}
case tea.KeyUp:
if m.cursor > 0 {
m.cursor--
}
case tea.KeyEnter:
m.chosen = m.items[m.cursor]
}
}
return m, nil
}
func (m *listModel) View() string {
s := ""
for i, item := range m.items {
cursor := " "
if i == m.cursor {
cursor = "> "
}
s += cursor + item + "\n"
}
if m.chosen != "" {
s += "\nSelected: " + m.chosen
}
return s
}
func main() {
t := &testing.T{}
model := newListModel()
// Harness provides a fluent API for test scripts.
btest.NewHarness(t, model, 40, 10).
Keys("down", "down").
Expect("Gamma").
Done()
fmt.Println("harness done")
}
Output: harness done
func (*Harness) Advance ¶
Advance is a placeholder for time-based drivers. It accepts a duration so call sites read naturally; the underlying TestModel does not yet integrate a FakeClock directly, but tests that own the FakeClock can chain .Advance(d) purely for documentation.
func (*Harness) Done ¶
func (h *Harness) Done()
Done runs registered teardown callbacks in LIFO order. Safe to call multiple times; subsequent calls are no-ops.
func (*Harness) Expect ¶
Expect asserts that the current screen contains text anywhere. Fails the test with a helpful diff if not.
func (*Harness) OnSetup ¶
OnSetup registers a function to run once when the first action fires. Use this for things like seeding state that depends on Harness being wired up but shouldn't run in NewHarness.
func (*Harness) OnTeardown ¶
OnTeardown registers a function to run when Done() is called. Registered functions run in reverse order (last-registered first) so they match a deferred-cleanup mental model.
func (*Harness) Screen ¶
Screen returns the current screen for ad-hoc assertions outside the fluent API.
type IndexedRow ¶
IndexedRow pairs a row index with its text content.
type KeyNavConfig ¶ added in v0.2.12
type KeyNavConfig struct {
// giving up on finding a cycle. Defaults to 50.
MaxTabs int
// Cols and Lines set the terminal dimensions. Defaults to 80x24.
ForwardKey string
BackwardKey string
}
KeyNavConfig configures the keyboard navigation verifier.
type KeyNavResult ¶ added in v0.2.12
type KeyNavResult struct {
Screens []string
UniqueScreens int
// repeats (i.e., the focus cycle length). Zero if no cycle was found
// within maxTabs presses.
CycleLength int
// in the rendered screen. A good component should change on every tab.
FocusChanges int
}
KeyNavResult holds the outcome of a keyboard navigation check.
func CheckKeyNav ¶ added in v0.2.12
func CheckKeyNav(t testing.TB, model tea.Model, cfg KeyNavConfig) *KeyNavResult
CheckKeyNav exercises keyboard navigation on a model by pressing tab repeatedly and tracking screen changes. It detects the focus cycle length and reports how many tab presses produce visible focus changes.
type Mutation ¶ added in v0.2.12
type Mutation struct {
// Type identifies the mutation kind.
Type MutationType
// Description is a human-readable explanation.
Description string
}
Mutation describes a single behavioral change applied to a model.
type MutationConfig ¶ added in v0.2.12
type MutationConfig struct {
// Test is the test function to run against each mutated model.
// It should exercise the model and make assertions using t.
Test func(t testing.TB, model tea.Model)
// Mutations limits which mutation types to apply. If nil, all built-in
// mutations are used.
Mutations []MutationType
// Cols and Lines set the terminal size for built-in mutations that
// need to construct a TestModel internally. Defaults to 80x24.
Cols int
Lines int
}
MutationConfig configures a mutation test run.
type MutationReport ¶ added in v0.2.12
type MutationReport struct {
Results []MutationResult
Killed int
Survived int
Total int
}
MutationReport summarizes the results of a full mutation test run.
func MutationTest ¶ added in v0.2.12
func MutationTest(t testing.TB, factory func() tea.Model, cfg MutationConfig) *MutationReport
MutationTest runs the test function against each mutation of the model and returns a report showing which mutations were killed (caught).
The factory function is called fresh for each mutation so mutations don't leak state between runs.
Example ¶
package main
import (
"fmt"
"testing"
"github.com/blitui/blit/btest"
tea "github.com/charmbracelet/bubbletea"
)
// listModel is a minimal model used in examples.
type listModel struct {
items []string
cursor int
chosen string
}
func newListModel() *listModel {
return &listModel{items: []string{"Alpha", "Beta", "Gamma"}}
}
func (m *listModel) Init() tea.Cmd { return nil }
func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if km, ok := msg.(tea.KeyMsg); ok {
switch km.Type {
case tea.KeyDown:
if m.cursor < len(m.items)-1 {
m.cursor++
}
case tea.KeyUp:
if m.cursor > 0 {
m.cursor--
}
case tea.KeyEnter:
m.chosen = m.items[m.cursor]
}
}
return m, nil
}
func (m *listModel) View() string {
s := ""
for i, item := range m.items {
cursor := " "
if i == m.cursor {
cursor = "> "
}
s += cursor + item + "\n"
}
if m.chosen != "" {
s += "\nSelected: " + m.chosen
}
return s
}
func main() {
t := &testing.T{}
factory := func() tea.Model { return newListModel() }
report := btest.MutationTest(t, factory, btest.MutationConfig{
Mutations: []btest.MutationType{btest.MutationEmptyView, btest.MutationDropKey},
Test: func(t testing.TB, model tea.Model) {
tm := btest.NewTestModel(t, model, 40, 10)
btest.AssertContains(t, tm.Screen(), "Alpha")
tm.SendKey("down")
tm.SendKey("enter")
btest.AssertContains(t, tm.Screen(), "Selected")
},
})
fmt.Printf("killed=%d total=%d score=%.0f%%\n", report.Killed, report.Total, report.Score())
}
Output: killed=2 total=2 score=100%
func (*MutationReport) Score ¶ added in v0.2.12
func (r *MutationReport) Score() float64
Score returns the mutation score as a percentage (killed / total * 100).
func (*MutationReport) Summary ¶ added in v0.2.12
func (r *MutationReport) Summary() string
Summary returns a human-readable summary of the mutation test.
func (*MutationReport) Survivors ¶ added in v0.2.12
func (r *MutationReport) Survivors() []MutationResult
Survivors returns all mutations that were not caught by the test suite.
type MutationResult ¶ added in v0.2.12
type MutationResult struct {
Mutation Mutation
// Killed is true if the test suite detected the mutation (test failed).
Killed bool
}
MutationResult is the outcome of applying a single mutation.
type MutationType ¶ added in v0.2.12
type MutationType int
MutationType identifies the kind of mutation applied.
const ( // MutationDropKey silently drops a key message. MutationDropKey MutationType = iota // MutationSwapKeys swaps a common key pair (e.g., up↔down). MutationSwapKeys // MutationEmptyView forces View() to return an empty string. MutationEmptyView // MutationStaticView forces View() to return a fixed string. MutationStaticView // MutationDropCmd discards commands returned from Update. MutationDropCmd // MutationNilInit makes Init() return nil. MutationNilInit )
func (MutationType) String ¶ added in v0.2.12
func (mt MutationType) String() string
String returns a human-readable label for the mutation type.
type PendingGolden ¶
type PendingGolden struct {
// GoldenPath is the path to the accepted golden file (may not exist yet
// if the golden was never written).
GoldenPath string
// NewPath is the .golden.new file written by AssertGolden on mismatch.
NewPath string
// Expected is the current accepted content (empty if golden doesn't exist).
Expected string
// Actual is the candidate content from the .golden.new file.
Actual string
}
PendingGolden describes a single pending snapshot review item: the canonical .golden path plus the candidate .golden.new content.
func FindPendingGoldens ¶
func FindPendingGoldens(root string) ([]PendingGolden, error)
FindPendingGoldens walks root recursively and returns all .golden.new files as PendingGolden items. root is typically "." (the package under test).
func (PendingGolden) Accept ¶
func (p PendingGolden) Accept() error
Accept writes Actual atomically to GoldenPath and removes NewPath. It uses a temp-file + rename so the update is atomic.
func (PendingGolden) Reject ¶
func (p PendingGolden) Reject() error
Reject removes the .golden.new file without touching the accepted golden.
func (PendingGolden) TestName ¶
func (p PendingGolden) TestName() string
TestName returns a short human-readable label derived from the golden path. It strips the leading testdata/ prefix and the .golden suffix.
type RGB ¶ added in v0.2.12
type RGB struct {
R, G, B uint8
}
RGB represents a color as 8-bit red, green, blue channels.
func ANSI256ToRGB ¶ added in v0.2.12
ANSI256ToRGB converts an ANSI 256-color index to an RGB value. Colors 0–15 are the standard terminal palette (implementation-defined; we use the xterm defaults). Colors 16–231 are a 6×6×6 color cube. Colors 232–255 are a grayscale ramp.
func SimulateColorBlind ¶ added in v0.2.12
func SimulateColorBlind(c RGB, typ ColorBlindType) RGB
SimulateColorBlind transforms an RGB color to simulate how it appears to a person with the specified color vision deficiency. Uses the Brettel/Viénot/Mollon simulation matrices.
Example ¶
package main
import (
"fmt"
"github.com/blitui/blit/btest"
)
func main() {
red := btest.RGB{R: 255, G: 0, B: 0}
simulated := btest.SimulateColorBlind(red, btest.Protanopia)
fmt.Printf("protanopia red → R=%d G=%d B=%d\n", simulated.R, simulated.G, simulated.B)
}
Output: protanopia red → R=198 G=197 B=0
type RealClock ¶
type RealClock struct{}
RealClock is a Clock backed by the real time package. Safe for concurrent use.
type Region ¶
type Region struct {
// contains filtered or unexported fields
}
Region is a rectangular sub-area of a Screen for scoped assertions.
func (*Region) CountOccurrences ¶
CountOccurrences returns how many times text appears in the region.
func (*Region) FindText ¶
FindText returns the (relativeRow, relativeCol) of the first occurrence of text within the region. Returns (-1, -1) if not found.
func (*Region) Row ¶
Row returns the text content of a row within the region (0-indexed relative to the region's top-left corner), trimmed of trailing spaces.
type Report ¶
type Report struct {
Suite string
StartedAt time.Time
Results []TestResult
}
Report is a collection of test results plus suite metadata.
func (*Report) TotalDuration ¶
TotalDuration sums the duration of all results.
func (*Report) WriteJUnit ¶
WriteJUnit writes the report as a JUnit XML file at path. Parent directories are created as needed.
type Screen ¶
type Screen struct {
// contains filtered or unexported fields
}
Screen wraps go-te to provide a virtual terminal for testing TUI output.
func (*Screen) Column ¶
Column extracts a vertical column of text from startRow to endRow (exclusive).
func (*Screen) ContainsAt ¶
ContainsAt reports whether the given text appears starting at (row, col).
func (*Screen) CountOccurrences ¶
CountOccurrences returns how many times text appears on the screen.
func (*Screen) FindAllText ¶
FindAllText returns all (row, col) positions where text appears.
func (*Screen) FindRegexp ¶
FindRegexp returns the (row, col) of the first regexp match. Returns (-1, -1) if not found.
func (*Screen) FindText ¶
FindText returns the (row, col) of the first occurrence of text on the screen. Returns (-1, -1) if not found.
Example ¶
package main
import (
"fmt"
"testing"
"github.com/blitui/blit/btest"
tea "github.com/charmbracelet/bubbletea"
)
// listModel is a minimal model used in examples.
type listModel struct {
items []string
cursor int
chosen string
}
func newListModel() *listModel {
return &listModel{items: []string{"Alpha", "Beta", "Gamma"}}
}
func (m *listModel) Init() tea.Cmd { return nil }
func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if km, ok := msg.(tea.KeyMsg); ok {
switch km.Type {
case tea.KeyDown:
if m.cursor < len(m.items)-1 {
m.cursor++
}
case tea.KeyUp:
if m.cursor > 0 {
m.cursor--
}
case tea.KeyEnter:
m.chosen = m.items[m.cursor]
}
}
return m, nil
}
func (m *listModel) View() string {
s := ""
for i, item := range m.items {
cursor := " "
if i == m.cursor {
cursor = "> "
}
s += cursor + item + "\n"
}
if m.chosen != "" {
s += "\nSelected: " + m.chosen
}
return s
}
func main() {
t := &testing.T{}
model := newListModel()
tm := btest.NewTestModel(t, model, 40, 10)
row, col := tm.Screen().FindText("Beta")
fmt.Printf("Beta at row=%d col=%d\n", row, col)
}
Output: Beta at row=1 col=2
func (*Screen) MatchesRegexp ¶
MatchesRegexp reports whether the screen content matches the regular expression.
func (*Screen) NonEmptyRows ¶
func (s *Screen) NonEmptyRows() []IndexedRow
NonEmptyRows returns only the non-empty rows with their row indices.
func (*Screen) Render ¶
Render feeds View() output (with ANSI codes) into the virtual terminal. It translates bare \n to \r\n (mimicking the terminal's ONLCR flag) so that Bubble Tea View() output renders correctly.
func (*Screen) String ¶
String returns a plain text representation of the entire screen (for debugging/golden files).
type Session ¶
type Session struct {
Version int `json:"version"`
Cols int `json:"cols"`
Lines int `json:"lines"`
Name string `json:"name,omitempty"` // v2+: logical session name
RecordedAt string `json:"recorded_at,omitempty"` // v2+: RFC3339 timestamp
Command []string `json:"command,omitempty"` // v2+: recorded command args
Steps []SessionStep `json:"steps"`
}
Session is the on-disk representation of a .tuisess file.
func LoadSession ¶
LoadSession reads a .tuisess file. Versions 1 and 2 are both accepted; v1 files are loaded without the metadata fields introduced in v2.
type SessionRecorder ¶
type SessionRecorder struct {
// contains filtered or unexported fields
}
SessionRecorder captures input + screen steps against a live TestModel.
func NewSessionRecorder ¶
func NewSessionRecorder(tm *TestModel) *SessionRecorder
NewSessionRecorder returns a recorder bound to an existing TestModel.
Example ¶
package main
import (
"fmt"
"testing"
"github.com/blitui/blit/btest"
tea "github.com/charmbracelet/bubbletea"
)
// listModel is a minimal model used in examples.
type listModel struct {
items []string
cursor int
chosen string
}
func newListModel() *listModel {
return &listModel{items: []string{"Alpha", "Beta", "Gamma"}}
}
func (m *listModel) Init() tea.Cmd { return nil }
func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if km, ok := msg.(tea.KeyMsg); ok {
switch km.Type {
case tea.KeyDown:
if m.cursor < len(m.items)-1 {
m.cursor++
}
case tea.KeyUp:
if m.cursor > 0 {
m.cursor--
}
case tea.KeyEnter:
m.chosen = m.items[m.cursor]
}
}
return m, nil
}
func (m *listModel) View() string {
s := ""
for i, item := range m.items {
cursor := " "
if i == m.cursor {
cursor = "> "
}
s += cursor + item + "\n"
}
if m.chosen != "" {
s += "\nSelected: " + m.chosen
}
return s
}
func main() {
t := &testing.T{}
model := newListModel()
tm := btest.NewTestModel(t, model, 40, 10)
// Record a session.
rec := btest.NewSessionRecorder(tm)
rec.Key("down")
rec.Key("enter")
fmt.Println("recorded 2 actions")
}
Output: recorded 2 actions
func (*SessionRecorder) Key ¶
func (r *SessionRecorder) Key(key string) *SessionRecorder
Key sends a named key and records the key + resulting screen.
func (*SessionRecorder) Resize ¶
func (r *SessionRecorder) Resize(cols, lines int) *SessionRecorder
Resize updates the simulated terminal size and records the step.
func (*SessionRecorder) Save ¶
func (r *SessionRecorder) Save(path string) error
Save writes the recorded session to the given path as a .tuisess file. Missing parent directories are created. If path has no extension it is given ".tuisess".
func (*SessionRecorder) Type ¶
func (r *SessionRecorder) Type(text string) *SessionRecorder
Type sends text one char at a time and records the aggregated step.
type SessionStep ¶
type SessionStep struct {
Kind string `json:"kind"` // "key", "type", "resize", "screen"
Key string `json:"key,omitempty"` // for kind=key
Text string `json:"text,omitempty"` // for kind=type
Cols int `json:"cols,omitempty"` // for kind=resize
Lines int `json:"lines,omitempty"` // for kind=resize
Screen string `json:"screen,omitempty"` // for kind=screen (expected post-state)
}
SessionStep is a single input or screen assertion.
type SmokeOpts ¶ added in v0.1.23
type SmokeOpts struct {
// Sizes to test during resize checks. Defaults to standard sizes if nil.
Sizes []SmokeSize
// Keys to test during key dispatch checks. Defaults to common navigation keys if nil.
Keys []string
}
SmokeOpts configures the smoke test suite.
type SmokeReport ¶ added in v0.1.23
type SmokeReport struct {
Results []SmokeResult
Passed int
Failed int
}
SmokeReport holds the results of a full smoke test run.
func Smoke ¶ added in v0.1.23
Smoke runs a standard smoke test suite against a tea.Model. It verifies that the model can render, handle key events, handle resize events, and handle mouse events without panicking. Returns a SmokeReport with results.
Use this for quick validation that a model is wired up correctly.
type SmokeResult ¶ added in v0.1.23
SmokeResult holds the outcome of a single smoke test check.
type Stopwatch ¶
type Stopwatch struct {
// contains filtered or unexported fields
}
Stopwatch measures elapsed time for performance assertions in tests.
func StartStopwatch ¶
func StartStopwatch() Stopwatch
StartStopwatch begins a new timing measurement.
func (Stopwatch) AssertUnder ¶
AssertUnder fails the test if the elapsed time exceeds the given duration.
type TestModel ¶
type TestModel struct {
// contains filtered or unexported fields
}
TestModel wraps a tea.Model for easy testing.
func NewTestModel ¶
NewTestModel creates a test wrapper around a Bubble Tea model. Calls Init() and processes any returned commands that produce messages.
Example ¶
package main
import (
"fmt"
"testing"
"github.com/blitui/blit/btest"
tea "github.com/charmbracelet/bubbletea"
)
// listModel is a minimal model used in examples.
type listModel struct {
items []string
cursor int
chosen string
}
func newListModel() *listModel {
return &listModel{items: []string{"Alpha", "Beta", "Gamma"}}
}
func (m *listModel) Init() tea.Cmd { return nil }
func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if km, ok := msg.(tea.KeyMsg); ok {
switch km.Type {
case tea.KeyDown:
if m.cursor < len(m.items)-1 {
m.cursor++
}
case tea.KeyUp:
if m.cursor > 0 {
m.cursor--
}
case tea.KeyEnter:
m.chosen = m.items[m.cursor]
}
}
return m, nil
}
func (m *listModel) View() string {
s := ""
for i, item := range m.items {
cursor := " "
if i == m.cursor {
cursor = "> "
}
s += cursor + item + "\n"
}
if m.chosen != "" {
s += "\nSelected: " + m.chosen
}
return s
}
func main() {
// Create a TestModel to drive a tea.Model synchronously.
t := &testing.T{}
model := newListModel()
tm := btest.NewTestModel(t, model, 40, 10)
// Send keys and inspect the screen.
tm.SendKey("down")
tm.SendKey("enter")
fmt.Println(model.chosen)
}
Output: Beta
func (*TestModel) AdvanceClock ¶ added in v0.2.12
AdvanceClock advances the attached FakeClock by d and sends a TickMsg to the model. Any timers registered with AfterFunc that cross their deadline will fire. Panics if no clock is attached.
func (*TestModel) Clock ¶ added in v0.2.12
Clock returns the attached FakeClock, or nil if none was set.
func (*TestModel) RequireScreen ¶
RequireScreen renders the model and runs assertions against the screen in one call. The callback receives the screen and can use assert helpers on it.
func (*TestModel) SendKey ¶
SendKey sends a key event to the model. Supports special key names like "enter", "tab", "up", "down", "left", "right", "esc", "backspace", "space", and single characters.
func (*TestModel) SendKeys ¶
SendKeys sends a sequence of named keys. Each key is processed individually. Example: tm.SendKeys("down", "down", "enter")
func (*TestModel) SendMouse ¶
func (tm *TestModel) SendMouse(x, y int, button tea.MouseButton)
SendMouse sends a mouse event to the model.
func (*TestModel) SendMsg ¶
SendMsg sends an arbitrary tea.Msg to the model and processes any resulting command.
func (*TestModel) SendResize ¶
SendResize sends a window resize event.
func (*TestModel) TriggerTick ¶ added in v0.2.12
func (tm *TestModel) TriggerTick()
TriggerTick sends a TickMsg with the current clock time (or time.Now if no clock is attached). Use this to simulate a timer/poller tick.
type TestResult ¶
type TestResult struct {
Name string
Package string
Duration time.Duration
Passed bool
Failure string
Skipped bool
Before string // optional screen before failure
After string // optional screen after failure
}
TestResult describes one test outcome used by the reporters.
type TickMsg ¶ added in v0.2.12
TickMsg is sent by TriggerTick and AdvanceClock to simulate timer events. Components can type-assert on this message to handle ticks deterministically in tests.
type TickMsgPlaceholder ¶
TickMsgPlaceholder is a btest-local tick shape used by Harness.Advance so we don't depend on an external TickMsg definition here. Consumers that care about tick semantics should use SendMsg directly.