#![cfg(unix)]
mod common;
use std::time::Duration;
use common::pty::{FakeTerminal, PtySession, wait_for, wait_for_in_order};
fn rat_bin() -> String {
assert_cmd::cargo::cargo_bin("rat").display().to_string()
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
needle.len() <= haystack.len() && haystack.windows(needle.len()).any(|w| w == needle)
}
fn drain_for(session: &PtySession, total: Duration) -> Vec<u8> {
let deadline = std::time::Instant::now() + total;
let mut out = Vec::new();
loop {
let now = std::time::Instant::now();
if now >= deadline {
return out;
}
out.extend(session.read_available(deadline - now));
}
}
#[test]
fn the_hardware_cursor_rests_on_the_caret() {
let session = PtySession::spawn(
&rat_bin(),
&["input", "--prompt", "> ", "--value", "abc"],
&[("RAT_APPEARANCE", "dark")],
)
.expect("spawn rat input under a pty");
let mut terminal = FakeTerminal::dark();
assert!(
wait_for(
&session,
&mut terminal,
b"\x1b[1A\r\x1b[5C\x1b[?25h",
Duration::from_secs(5),
),
"the first frame must park a visible cursor on the caret cell"
);
session.write_bytes(b"\x1b[D");
assert!(
wait_for(
&session,
&mut terminal,
b"\r\x1b[4C",
Duration::from_secs(5),
),
"moving the caret must hop the visible cursor onto it"
);
session.write_bytes(b"\r");
let tail = drain_for(&session, Duration::from_secs(5));
assert!(
contains(&tail, b"abc"),
"the submitted value must reach stdout: {:?}",
String::from_utf8_lossy(&tail)
);
assert!(
!session.kill_if_alive(Duration::from_secs(5)),
"rat input must exit on its own after Enter"
);
}
#[test]
fn a_wide_prompt_glyph_does_not_shift_the_value() {
let session = PtySession::spawn(
&rat_bin(),
&["input", "--prompt", "日", "--value", "abc"],
&[("RAT_APPEARANCE", "dark")],
)
.expect("spawn rat input under a pty");
let mut terminal = FakeTerminal::dark();
let adjacent = "日\x1b[0mabc".as_bytes();
wait_for_in_order(
&session,
&mut terminal,
&[adjacent, b"\x1b[5C"],
Duration::from_secs(5),
);
session.write_bytes(b"\r");
let _ = drain_for(&session, Duration::from_millis(300));
session.kill_if_alive(Duration::from_secs(5));
}
#[test]
fn a_full_field_scrolls_and_keeps_the_caret_past_the_text() {
let session = PtySession::spawn(
&rat_bin(),
&["input", "--prompt", "> "],
&[("RAT_APPEARANCE", "dark")],
)
.expect("spawn rat input under a pty");
let mut terminal = FakeTerminal::dark();
session.set_winsize(10, 20);
assert!(
wait_for(&session, &mut terminal, b"\x1b[2C", Duration::from_secs(5)),
"the empty field parks the caret at the prompt edge"
);
session.write_bytes(b"abcdefghijklmnopqr");
let scrolled = b"\x1b[0mbcdefghijklmnopqr";
wait_for_in_order(
&session,
&mut terminal,
&[scrolled, b"\x1b[19C"],
Duration::from_secs(5),
);
session.write_bytes(b"\r");
let _ = drain_for(&session, Duration::from_millis(300));
session.kill_if_alive(Duration::from_secs(5));
}
fn wait_for_bytes(
session: &PtySession,
terminal: &mut FakeTerminal,
needle: &[u8],
timeout: Duration,
) -> Option<Vec<u8>> {
let deadline = std::time::Instant::now() + timeout;
let mut seen: Vec<u8> = Vec::new();
loop {
let now = std::time::Instant::now();
if now >= deadline {
return None;
}
let chunk = session.read_available((deadline - now).min(Duration::from_millis(50)));
if chunk.is_empty() {
continue;
}
terminal.respond(session, &chunk);
seen.extend_from_slice(&chunk);
if contains(&seen, needle) {
return Some(seen);
}
}
}
#[test]
fn the_input_frame_carries_no_reverse_video() {
let session = PtySession::spawn(
&rat_bin(),
&["input", "--prompt", "> ", "--value", "abc"],
&[("RAT_APPEARANCE", "dark")],
)
.expect("spawn rat input under a pty");
let mut terminal = FakeTerminal::dark();
let seen = wait_for_bytes(
&session,
&mut terminal,
b"\x1b[1A\r\x1b[5C\x1b[?25h",
Duration::from_secs(5),
)
.expect("the first frame parks the cursor");
assert!(
!contains(&seen, b"\x1b[7m"),
"the frame must not paint a caret cell: {:?}",
String::from_utf8_lossy(&seen)
);
session.write_bytes(b"\r");
let _ = drain_for(&session, Duration::from_millis(300));
session.kill_if_alive(Duration::from_secs(5));
}