documentation/TUI-TESTS.md · main · 2026-10-13

TUI Test Suite

Headless integration tests drive the real TUI model over a seeded git repository and assert on the rendered output, with no terminal.

Running · Layout · Harness · Fixture · Inventory · Notes

Running

go test ./library/tui/test/                              # the quick tier
GITSOCIAL_TEST_FULL=1 go test ./library/tui/test/        # the full tier: adds TestSmoke, TestSequence, TestGolden/LayoutProperties
go test ./library/tui/test/ -run Golden -update          # regenerate the golden files
go test ./library/tui/test/ -run TestGenerateFixture -generate   # regenerate the fixture tarballs
scripts/test.sh -run Smoke ./library/tui/test/           # any of the above with streamed per-test progress

The tests create temporary directories and need no external services. Run times with a warm build cache:

RunTime
quick tier13 s
full tier78 s
TestSmoke40 s
TestGolden/LayoutProperties20 s

Layout

library/tui/test/
├── harness.go            # the headless model driver
├── fixture.go            # repository setup and data seeding
├── assert.go             # render assertions: ANSI stripping, pattern matching
├── main_test.go          # the shared fixture, via TestMain
├── generate_test.go      # fixture tarball generation, behind -generate
├── smoke_test.go         # every key on every view
├── display_test.go       # seeded content renders
├── golden_test.go        # golden files and layout properties
├── navigation_test.go    # view transitions
├── sequence_test.go      # multi-step interactions
├── form_test.go          # form submits
├── diff_test.go          # diff view keys
├── cursor_test.go        # list cursor stability
├── history_diff_test.go  # history-diff footers
├── stack_test.go         # stacked pull requests
├── proposal_test.go      # cross-repo proposals
├── anchors_test.go       # detail card anchors against its links
└── testdata/             # fixture-repo.tar.gz, fixture-fork.tar.gz, fixture.json, *.golden

Harness

Harness wraps tui.NewModel, sends tea.KeyMsg and tea.WindowSizeMsg directly, and reads the rendered output.

func New(t *testing.T, workdir, cacheDir string) *Harness    // 120x40, Init() run, startup commands drained

func (h *Harness) SendKey(key string)
func (h *Harness) SendKeys(keys ...string)
func (h *Harness) Navigate(path string)
func (h *Harness) NavigateTo(loc tuicore.Location)
func (h *Harness) SetSize(w, h int)
func (h *Harness) DrainCmds()

func (h *Harness) Rendered() string
func (h *Harness) CurrentPath() string
func (h *Harness) CurrentContext() tuicore.Context
func (h *Harness) CurrentView() tuicore.View
func (h *Harness) BindingsForContext(ctx tuicore.Context) []tuicore.Binding

Every SendKey and Navigate drains the commands it produces. Key names: enter, esc, tab, shift+tab, the arrows, ctrl+c, ctrl+d, ctrl+u, space, backspace, home, end, pgup, pgdown; every other string is sent as runes.

Assertions: stripANSI, rendered(h), assertContains(t, output, substr), assertRendersItem(t, h, loc, want...), assertNotEmpty, assertMaxWidth(t, output, maxCols), assertFitsTerminal(t, h), assertFrameFits(t, frame, cols, lines).

Prefer assertRendersItem. It navigates, requires every named fragment of seeded content, and fails on an empty expectation. assertNotEmpty passes when a view shows only its frame (borders, title and footer), so use it only for a view with no seeded data.

Fixture

TestMain extracts two repositories once per run, and the tests share them read-only:

Entity ids are in testdata/fixture.json.

func getFixture(t *testing.T) *Fixture      // the shared fixture, for read-only tests
func SetupFixture(t *testing.T) *Fixture    // a fresh copy, for tests that mutate the repository

SetupFixture closes the shared cache first and opens it again on cleanup; cache.Open is a no-op while a cache is open.

The extension APIs seed this data, taken from the protocol specs:

The cache is filled with client.SyncWorkspace, workspace first and fork second. A cross-repo edit resolves only after its canonical version is in the cache. Commit timestamps are rewritten one second apart, with the last one fixtureCommitAge before the run, so each row shows the same relative time for the full run and the order is deterministic. Fixture generation sets HOME, XDG_CONFIG_HOME and GITSOCIAL_PERSONAL_REPO to a temporary directory.

seedMemoTiersPanic builds the memo tiers that are outside the workspace repository: the personal bare repository, the session tui-fixture, and the repository behind the inherits ref. Each has one memo, written through the memo API. TestMain sets MEMO_SESSION_ID to that session, and the repositories are under the isolated HOME. They stay after a fixture ends, so setup keeps an existing repository and only re-indexes it. Their commit dates are fixtureCommitAge before the run. The session picker reads the session age from the repository, not from the cache.

Inventory

FileTestsCovers
smoke_test.goTestSmoke/AllKeysAllViews, GlobalShortcuts, UnregisteredKeysIgnoredeach view with the keys of its context and one key that nothing binds, then each global shortcut once, on the first view that binds it (86 subtests), with no panic; full tier only
display_test.goTestDisplay/*: Timeline, Search, MyRepository, Board, IssuesList, Milestones, Sprints, PRList, ReleasesList, Notifications, Memos, ProjectMemos, MemoDetail, MemoHistory, MemoInherits, Forks, Settings, Site, Cache, Helpseeded content appears on each view
golden_test.goTestGolden/{timeline,board,issues,pr_list,releases,settings,help,memo_personal,memo_session,memo_session_items,memo_inherited,memo_inherits}_120x40ANSI-stripped renders against testdata/*.golden, each frame also 120 columns wide and 40 lines; the memo goldens have one memo per tier and the inherited source
golden_test.goTestGolden/LayoutPropertieseach route fits the width and the height at 120x40, 80x24 and 200x60; full tier only
golden_test.goTestGoldenWidth_rejectsNavOneColumnTooWidea navigation row one column too wide fails the check
golden_test.goTestGoldenTime_labelHoldsForALongRunthe fixture keeps one relative-time label for the full run
navigation_test.goTestNavigation/GlobalKeys (S, P, R, V, M), Back, SiteEditToggle, MultiLevelBack, Detail, Search, Help, Notificationsthe global jump keys open their routes; esc goes back, and /, ? and @ open search, help and notifications
sequence_test.goTestSequence/*: AllExtensions, BrowseAndReturn, IssuesFlow, SettingsAndBack, QuickJumpOverridesHistory, the *OpensForm and *Navigates flows per item type, PostRetractShowsConfirm, SearchFlow, PRDiffNavigates, MultipleViewRenders, PushConfirmNamesRemotemulti-step flows; full tier only
form_test.goTestMilestoneFormCreates, TestSprintFormCreatesa submitted create form opens the detail view of the new item, on an isolated fixture
diff_test.goTestDiffTabMovesToNextFiletab in a diff moves the cursor to the next file and leaves the panel focus alone, on an isolated fixture with a two-file commit
cursor_test.goTestTimelineCursorSurvivesFetch, TestTimelineCursorSurvivesBackNavthe timeline selection stays after a fetch and after a return from a detail view
history_diff_test.goTestHistoryDiffFooter, PostHistoryDiffRendersevery history-diff context registers its footer entries without duplicates, and the view renders
stack_test.goTestStackDisplay/BadgeOnPRList, TestStackBindings, TestStackNavigationBackendthe stack badge, the stack keys, and GetStack and getDependents behind them
proposal_test.goTestProposalDisplay/{IssueListMarker,IssueDetailBanner,HistoryRow,HistoryFooterOffersAcceptAndDecline}, TestProposalAccept, TestProposalDeclinethe ✎ marker, the banner, the history row, and A and X that accept or decline a proposal, on an isolated fixture
anchors_test.goTest{Issue,PR,Milestone,Sprint,Release,Memo,Post}Detail_anchorsMatchLinksthe n-th anchor a detail card marks has the location of the n-th entry of its Links(), on each detail view with a body that holds a ref, a url and a mention; the issue carries an assignee, a label and a citing commit, the pull request a foreign head with a tip, a dependency, a reviewer, a label and a closed issue

Notes