use std::path::{Path, PathBuf};
use std::time::Duration;
use termlens::{Key, Screen, Terminal};
const TIMEOUT: Duration = Duration::from_secs(10);
const EXPECTED_UNSUPPORTED: [&str; 1] = ["^[[59m"];
fn fixture(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn spawn(run_dir: &str, size: (u16, u16)) -> termlens::Result<Terminal> {
let mut t = Terminal::builder()
.size(size.0, size.1)
.env_clear()
.timeout(TIMEOUT)
.arg(fixture(run_dir))
.spawn(env!("CARGO_BIN_EXE_launchbound-tui"))?;
t.wait_until(|s| s.contains("launchbound"))?;
Ok(t)
}
fn unsupported(screen: &Screen) -> Vec<String> {
screen.unsupported().iter().map(|s| s.to_string()).collect()
}
const VIEWS: [(char, &str); 3] = [
('2', "\u{250c}ranking"),
('3', "\u{250c}rejections"),
('4', "\u{250c}progress"),
];
fn check(label: &str, screen: &Screen) {
assert_eq!(
unsupported(screen),
EXPECTED_UNSUPPORTED,
"{label}: launchbound-tui emitted a sequence termlens does not \
model. Until it is understood, every golden in this crate is being \
held against a grid that may be wrong:\n{screen}"
);
assert_eq!(
screen.unsupported_overflow(),
0,
"{label}: the record is complete, not truncated"
);
}
#[test]
fn the_emulator_drops_nothing_that_could_change_a_cell() -> termlens::Result<()> {
for (run, size) in [
("run-flip", (80u16, 24u16)),
("run-flip", (60, 30)),
("run-metal", (80, 24)),
("run-metal", (60, 30)),
] {
let mut t = spawn(run, size)?;
let first = t.wait_frame(|s| s.contains("candidates ·"))?;
check(&format!("{run} {}x{} overview", size.0, size.1), &first);
for (key, needle) in VIEWS {
t.send(Key::Char(key))?;
let frame = t.wait_frame(|s| s.contains(needle))?;
check(&format!("{run} {}x{} view {key}", size.0, size.1), &frame);
}
t.send(Key::Char('q'))?;
assert!(t.wait_exit()?.success());
check(
&format!("{run} {}x{} after exit", size.0, size.1),
&t.screen(),
);
}
Ok(())
}
#[test]
fn the_tui_leaves_the_terminal_modes_alone() -> termlens::Result<()> {
let mut t = spawn("run-flip", (80, 24))?;
let screen = t.wait_frame(|s| s.contains("q quit"))?;
assert!(!screen.insert_mode(), "launchbound-tui never sets IRM");
assert_eq!(screen.visual_bells(), 0, "no visual bell");
assert_eq!(screen.bells(), 0, "and no audible one either");
assert!(
screen.mouse_modes().is_empty(),
"the TUI enables no mouse reporting, got {:?}",
screen.mouse_modes()
);
assert!(!screen.bracketed_paste(), "and no bracketed paste");
assert!(!screen.focus_events(), "and no focus reporting");
assert!(
!screen.application_cursor(),
"and no application cursor keys"
);
assert!(!screen.cursor().2, "the cursor is hidden while drawing");
t.send(Key::Char('q'))?;
assert!(t.wait_exit()?.success());
Ok(())
}
#[test]
fn the_renderer_wraps_the_text_itself_so_no_terminal_row_is_wrapped() -> termlens::Result<()> {
let mut t = spawn("run-flip", (60, 30))?;
t.wait_frame(|s| s.contains("candidates ·"))?;
t.send(Key::Char('3'))?;
let screen = t.wait_frame(|s| s.contains("all refused configurations:"))?;
let wrapped: Vec<u16> = (0..screen.rows())
.filter(|row| screen.row_wrapped(*row))
.collect();
assert!(
wrapped.is_empty(),
"rows {wrapped:?} are terminal-wrapped. The renderer used to break \
lines itself, and `a_refusal_reason_survives_a_narrow_terminal_whole` \
rebuilds the prose on that assumption — reread it before changing \
this:\n{screen}"
);
t.send(Key::Char('q'))?;
assert!(t.wait_exit()?.success());
Ok(())
}
#[test]
fn a_frame_survives_the_snapshot_format_and_json() -> termlens::Result<()> {
let mut t = spawn("run-metal", (80, 24))?;
let screen = t.wait_frame(|s| s.contains("q quit"))?;
let saved = screen.with_styles().to_string();
let parsed = Screen::parse(&saved)?;
assert!(screen.diff(&parsed).is_empty(), "{}", screen.diff(&parsed));
assert_eq!(parsed.with_styles().to_string(), saved, "byte for byte");
let json = serde_json::to_string(&screen).expect("a Screen serializes");
let back: Screen = serde_json::from_str(&json).expect("and comes back");
assert!(screen.diff(&back).is_empty(), "{}", screen.diff(&back));
let banner = screen.cell(1, 0).expect("the banner's first cell");
assert!(banner.style().bold && banner.style().reverse);
for other in [&parsed, &back] {
let cell = other.cell(1, 0).expect("the banner's first cell, restored");
assert_eq!(
cell.style(),
banner.style(),
"the no-gate banner came back unstyled: a saved screen would \
read as an ordinary line of text"
);
}
t.send(Key::Char('q'))?;
assert!(t.wait_exit()?.success());
Ok(())
}