Testing
/** Runs `v` to its end on the Kyo scheduler (at most 60 s). */
private def run[A](v: => A < (Async & Scope))(using Frame): A =
import AllowUnsafe.embrace.danger
KyoApp.Unsafe.runAndBlock(60.seconds)(v).getOrThrow
private def start(ui: => UI < (Async & Scope))(using
Frame,
): Headless < (Async & Scope) =
Headless.start(60, 10)(_ => ui)
/** Waits until the screen shows `text`, at most 10 s. */
private def shows(h: Headless, text: String)(using Frame): Boolean < Async =
h.awaitScreen(_.contains(text), 10.seconds)
test(
"search: the results of the quiet query; a new query gives new results",
) {
val ok = run {
for
h <- start(Recipes.search(q => Chunk(s"$q-1", s"$q-2")))
idle <- shows(h, "type to search")
_ <- h.typeText("kyo")
found <- shows(h, "found: kyo-1, kyo-2")
_ <- h.key(Keyboard.Backspace)
again <- shows(h, "found: ky-1, ky-2")
yield (idle, found, again)
}
assertEquals(ok, (true, true, true))
}
test("progress: 100 writes in a row; the last value shows") {
val ok = run {
for
done <- Signal.initRef(0)
h <- start(Recipes.progress(done))
_ <- Kyo.foreach(1 to 100)(done.set)
last <- shows(h, "100 %")
yield last
}
assert(ok)
}
run runs a Kyo value to its end inside a munit test. start starts the app on the headless test kit, on a screen of 60 by 10 cells. The first test types into the search box and waits for the results. The second test writes a SignalRef of the app 100 times and waits until the screen shows the last value.
The headless test kit
tui4s.testkit.Headless runs an app with no terminal. Headless.start(width, height)(ui) gives a running app. Its ui gets the quit action, as the ui of Tui.run does.
| Method | What it does |
|---|---|
press(event), key(k), key(k, mods), char(c) | Sends one key, and waits for its frame. |
typeText(s) | Sends each character of s as a key. |
paste(text) | Sends a bracketed paste: one edit. |
resize(w, h) | Changes the size of the screen. |
screen | Gives the current screen. It does not wait. |
awaitScreen(p, timeout) | Waits until the screen satisfies p, at most timeout. Use it after a write to a SignalRef. |
awaitLog | Gives the lines of the log of the run. |
finished, awaitEnd(timeout) | Tells if the run has ended. |
A key is one event: its handlers run before the frame, so the frame of the key holds all of their writes. A write from the test is not a key, so the test waits for the screen with awaitScreen. The kit itself never waits unless a method name says so. After the end of the run, a method that waits for a frame fails at once.
kyo-test and munit
The examples use munit. tui4s uses kyo-test for its own suites, and kyo-test-prop for the property tests (PropertyTest, Gen, Shrink, with explicit Gen values). The kit gives Kyo values, so it works with both.
The real terminal
The PTY kit runs the examples in a pseudo-terminal, with no human. It sends keys, checks raw mode from outside while the app runs, compares stty -a before and after each run, and checks the screens:
examples/ptycheck/run.sh
It needs expect, python3 and JDK 25. CI runs it in the terminal job. The terminal casts on these pages come from the same pseudo-terminal runs (scripts/docs-casts.sh).