Testing #
Because the runtime core is synchronous — events in, escape bytes out — entire apps are testable without a terminal, a TTY, or an executor. The same harness eye-declare tests itself with is public:
[dev-dependencies]
eye_declare_engine = { version = "...", features = ["test-util"] }
use eye_declare::{InputEvent, Runtime};
use eye_declare_engine::test_terminal::TestTerminal;
#[test]
fn submit_commits_the_line() {
let mut rt = Runtime::new(my_app(), 80, 24);
let mut term = TestTerminal::new(80, 24); // a real VTE emulator
term.feed(&rt.present());
let (bytes, _) = rt.handle(InputEvent::Key(enter()));
term.feed(&bytes);
let screen = term.viewport_lines().join("\n");
assert!(screen.contains("✓ hello"));
}
TestTerminal is a real terminal emulator (VTE): assertions run against
what a user would actually see, including scrollback
(term.scrollback_lines()), not against an internal buffer.
The one rule: feed every byte #
The runtime diffs against what it believes is on screen. Every byte it
returns — from handle, process, startup, not just present — must
reach the TestTerminal, or the emulator's state diverges from the
runtime's and later assertions fail confusingly. Wrap the pair in a small
harness so it's impossible to forget:
struct Harness { rt: Runtime<MyApp>, term: TestTerminal }
impl Harness {
fn press(&mut self, code: KeyCode) -> Option<Output> {
let (bytes, exit) = self.rt.handle(key_event(code));
self.term.feed(&bytes);
exit
}
}
Async flows, synchronously #
Spawned work delivers messages; tests deliver those messages themselves and skip the executor entirely:
h.press(KeyCode::Enter); // submit → app spawns request
h.process(Msg::Chunk("hello ".into())); // play the stream by hand
h.process(Msg::Chunk("world".into()));
h.process(Msg::StreamDone);
assert!(h.all_lines().contains("hello world"));
This makes timing-free tests of streaming, cancellation, and error paths:
the sequence of messages is the scenario. (Effects the app queued are
observable via Runtime::take_effects if a test cares; otherwise they're
inert.) For the stream-producing functions themselves, ordinary
#[tokio::test]s collecting the stream complete the picture.
What to assert #
- Screen content for rendering (
viewport_lines), scrollback + viewport for committed output. - Model state for logic — the app struct is right there:
rt.app().submitted == 2. - Byte-level cost when it matters:
present()on an unchanged tail should return (nearly) nothing, and a seal of already-displayed content should not repaint it. Regressions in output efficiency show up as assertions onbytes.len().