tuimark

Testing

Testing Tuimark views with golden dumps and tuimark test, Go tests on Dump, Validate and Play, fixtures for live data, and end-to-end terminal specs with Glyphrun.

Because every frame is deterministic data, a Tuimark view is easy to test at three levels:

LevelToolCatches
The documenttuimark test with golden dumpsany change to the geometry, text, or styles of a frame, at the sizes you care about
The hostgo test on Dump, Validate, Catalog, and Playa view that no longer loads, a missing handler, data that renders wrong, a key that does the wrong thing
The whole appGlyphrun specsthe real binary in a real terminal: keys, timing, quitting cleanly

Golden dumps

A golden is a dump saved to a file and committed. tuimark test renders each document again and compares, so any change to a frame shows up as a failing test and a readable diff.

The goldens are listed in a manifest, testdata/golden/manifest.json by default:

[
  {
    "name": "tasks",
    "file": "ui/app.tui",
    "data": "ui/sample.json",
    "sizes": ["40x24", "80x24", "120x30"],
    "json": ["80x24"]
  },
  {
    "name": "tasks-after-enter",
    "file": "ui/app.tui",
    "data": "ui/sample.json",
    "input": "down enter",
    "sizes": ["80x24"],
    "json": ["80x24"],
    "styles": true
  }
]

For every size in sizes, the text dump is compared with <dir>/<name>/<size>.txt; for the sizes also in json, the JSON dump with <size>.json.

FieldMeaning
name, file, sizesrequired: the golden's directory, the document, and the sizes to render
datathe JSON store, as --data
jsonthe sizes that also get a JSON golden
theme"dark" or "light", as --theme
input or scriptplay steps: the golden is then the frame after them, with the events that fired
styles, cellsinclude --styles or --cells in the JSON goldens
frozentrue means --update never rewrites this golden
tuimark test                      # compare; exit 2 on any mismatch
tuimark test --update             # rewrite the goldens that are not frozen
tuimark test path/to/goldens      # another directory with its own manifest.json

Each golden prints one PASS or FAIL line, with a short diff hint on failure.

Updating safely

--update protects the JSON goldens: it refuses to overwrite one unless the new dump is a superset of the old, with every existing value unchanged and every array the same length, only gaining members. If a node moved or text changed, it writes nothing for that size and fails naming the first difference as a JSON Pointer:

FAIL tasks 80x24 (json): update removes or changes /nodes/3/y

When the change is deliberate (you moved the node), review it and run tuimark test --update --allow-breaking once. Accidental layout changes can no longer slip in with an update.

Go tests

In the host's own tests, Validate, Dump, and Catalog check the view with the real data types, without a terminal. Dump is a pure snapshot, so it is safe to call at many sizes in one test. The Go API page has a complete example; a check that every action has a handler is a few more lines:

for _, a := range ui.Catalog() {
	if !a.Builtin && !a.Registered {
		t.Errorf("action %q (used by %v) has no handler", a.Name, a.Sources)
	}
}

Driving keys with Play (0.3)

Play runs the same steps as tuimark play against your app, with your handlers, and without a terminal. So the host's reactions are tested with the real keymap, not by calling handlers by hand:

func TestKillAsksFirst(t *testing.T) {
	ui := newTestApp(t) // Load or LoadFS, Bind the fixture data, On(...) every handler
	res, err := ui.Play(tuimark.PlayOptions{Cols: 100, Rows: 30}, "7", "K")
	if err != nil {
		t.Fatal(err)
	}
	if open, _ := ui.Get("kill_open"); open != true {
		t.Fatalf("K did not open the confirmation; events: %v", res.Events)
	}
	res, _ = ui.Play(tuimark.PlayOptions{Cols: 100, Rows: 30}, "enter")
	if len(res.Events) != 0 {
		t.Fatalf("enter inside the confirmation fired %v", res.Events)
	}
}

The state carries over between calls, so a test reads like a session: play some keys, check the store with Get or the frame in res.Dump, play more.

Fixtures for live data

A host that shows live data (metrics, processes, a network) needs a fixture mode for its tests and demos. The pattern that works:

  • Replace every source, not just the data. The clock or sampler, the thing that acts (a process killer, a file writer), the settings store, and the history all get fake versions. Then a test never starts a goroutine or touches the machine, and a demo never kills a real process.
  • Keep the real units. If the collector reports bytes per second, the fixture reports bytes per second too. A fixture that feeds already-scaled numbers hides every scaling bug in the view.
  • Seed it. A seeded generator gives the same frames every run, so goldens and Glyphrun specs stay stable.
  • Give the binary the switch, as in monitor studio --tuimark --fixture, so end-to-end specs run against the same data the Go tests use.

End-to-end with Glyphrun

Glyphrun runs a program in a real pseudo-terminal, types into it, waits for text on the screen, and checks outcomes, writing an artifact pack (screens, frames, an SVG of the final screen) that a person or an agent can read after a failure. Tuimark's own repository tests every example this way.

A spec for the host from getting started:

version: 1
name: hello_open
intent: |
  The checklist opens with the first task selected; moving down and pressing
  enter opens the second task, and q quits cleanly.
target:
  cmd: ["./bin/hello"]
  cwd: "."
  env:
    TUIMARK_THEME: dark
    TUIMARK_COLOR: truecolor
terminal:
  cols: 60
  rows: 12
steps:
  - wait:
      screen:
        contains: "launch checklist"
      timeoutMs: 5000
  - press: down
  - press: enter
  - wait:
      screen:
        contains: "opened t2"
      timeoutMs: 2000
  - snapshot: opened
  - press: q
  - wait:
      process:
        exitCode: 0
      timeoutMs: 3000
outcomes:
  - id: enter_opens_the_cursor_row
    description: enter on the second task fires open, and the host shows it in the title
    verify:
      screen:
        contains: "opened t2"
  - id: q_quits
    description: q is the built-in quit and the process exits 0
    verify:
      process:
        exitCode: 0
go install github.com/abdul-hamid-achik/glyphrun/cmd/glyph@latest
go build -o bin/hello .
glyph spec verify specs/hello_open.yml
glyph run specs/hello_open.yml

Pin TUIMARK_THEME and TUIMARK_COLOR in the spec (or in glyphrun.config.yml), so a run does not depend on the terminal of whoever runs it. glyph docs covers the spec format.

Which to write

  • For a view you are designing, let the agent loop (validate, dump, play) find problems as you go, then pin the result with goldens at 40, 80, and 120 columns.
  • For interactions, add goldens with input: they check the keymap, focus, and events, and run in milliseconds.
  • For the host, a Go test on Catalog and Dump with real data, and Play for what its handlers do when keys are pressed.
  • For the finished app, one Glyphrun spec per user journey: launching, the main task, quitting.
Edit on GitHub

Last updated on

On this page