use std::time::{Duration, Instant};
use termlens::{Error, Key, Terminal};
mod common;
use common as util;
fn spawn_form_echo() -> termlens::Result<Terminal> {
Terminal::builder()
.size(80, 24)
.timeout(Duration::from_secs(10))
.env_clear()
.spawn(util::fixture_bin("form-echo"))
}
#[test]
fn the_frame_completed_before_the_call_is_evaluated() -> termlens::Result<()> {
let mut t = spawn_form_echo()?;
t.wait_frame(|s| s.contains("form-echo ready"))?;
t.send(Key::Esc);
assert!(t.wait_exit()?.success());
Ok(())
}
#[test]
fn a_frame_is_internally_consistent() -> termlens::Result<()> {
let mut t = spawn_form_echo()?;
t.wait_frame(|s| s.contains("form-echo ready"))?;
t.send_str("hi");
t.wait_frame(|s| s.contains("input: hi"))?;
let frame_ok = std::cell::Cell::new(false);
t.wait_frame(|s| {
if s.contains("input: hi") {
frame_ok.set(s.contains("last: char:i"));
true
} else {
false
}
})?;
assert!(
frame_ok.get(),
"frame satisfied one row but not its sibling"
);
t.send(Key::Esc);
assert!(t.wait_exit()?.success());
Ok(())
}
#[test]
fn a_torn_repaint_is_never_observed() -> termlens::Result<()> {
let mut t = spawn_form_echo()?;
t.wait_frame(|s| s.contains("form-echo ready"))?;
t.send(Key::F(2));
let mut observed = None;
t.wait_frame(|s| {
if s.contains("torn: left") {
observed = Some(s.row_text(5));
true
} else {
false
}
})?;
let row = observed.expect("predicate matched");
assert!(
row.contains("torn: left right"),
"wait_frame observed a torn frame: {row:?}"
);
t.send(Key::Esc);
assert!(t.wait_exit()?.success());
Ok(())
}
#[test]
fn apps_without_synchronized_output_time_out_with_guidance() {
let mut t = Terminal::builder()
.size(80, 24)
.timeout(Duration::from_millis(500))
.env_clear()
.spawn(util::fixture_bin("hello-tui"))
.unwrap();
t.wait_until(|s| s.contains("╯")).unwrap();
let start = Instant::now();
let err = t.wait_frame(|_| true).unwrap_err();
assert!(start.elapsed() >= Duration::from_millis(500));
let msg = err.to_string();
assert!(msg.contains("2026"), "no guidance in: {msg}");
assert!(msg.contains("wait_until"), "no alternative named in: {msg}");
assert!(err.screen().is_some(), "timeout must still embed a screen");
t.send(Key::Char('q'));
assert!(t.wait_exit().unwrap().success());
}
#[test]
fn wait_frame_fails_fast_on_eof() {
let mut t = Terminal::builder()
.timeout(Duration::from_secs(30))
.args(["-c", r"printf '\033[?2026hdone\033[?2026l'; read guard"])
.spawn("sh")
.unwrap();
t.wait_frame(|s| s.contains("done")).unwrap();
t.send(Key::Enter);
let start = Instant::now();
let err = t.wait_frame(|s| s.contains("never painted")).unwrap_err();
assert!(matches!(err, Error::Eof { .. }), "expected Eof, got: {err}");
assert!(
start.elapsed() < Duration::from_secs(10),
"EOF should fail fast"
);
}
#[test]
fn wait_idle_does_not_resolve_inside_an_open_synchronized_update() {
let mut t = Terminal::builder()
.timeout(Duration::from_millis(600))
.args(["-c", r"printf '\033[?2026hhalf a frame'; read guard"])
.spawn("sh")
.unwrap();
t.wait_until(|s| s.contains("half a frame")).unwrap();
let err = t.wait_idle(Duration::from_millis(100)).unwrap_err();
assert!(
matches!(err, Error::Timeout { .. }),
"an open synchronized update must not count as idle: {err}"
);
}