facett-console 0.1.2

facett — themed terminal/shell component: fixed monospace cell grid, block/beam cursor, ANSI colour mapped to the palette, scrollback on the CygnusEd smooth-scroll engine
Documentation
//! Inject-and-assert tests: feed real ANSI lines + drive the scrollback engine
//! with an injected clock, asserting parsed spans, scrollback ring, copy text,
//! and headless render.

use super::*;

#[test]
fn parse_ansi_splits_coloured_runs() {
    // "ok " default, then green "PASS", then default " done".
    let spans = parse_ansi("ok \x1b[32mPASS\x1b[0m done");
    assert_eq!(spans.len(), 3);
    assert_eq!(spans[0].text, "ok ");
    assert_eq!(spans[0].color, None);
    assert_eq!(spans[1].text, "PASS");
    assert_eq!(spans[1].color, Some(AnsiColor::Green));
    assert_eq!(spans[2].text, " done");
    assert_eq!(spans[2].color, None);
}

#[test]
fn plain_line_is_one_default_span() {
    let spans = parse_ansi("hello world");
    assert_eq!(spans.len(), 1);
    assert_eq!(spans[0].text, "hello world");
    assert_eq!(spans[0].color, None);
}

#[test]
fn scrollback_ring_drops_oldest_past_max() {
    let mut c = Console::new("term");
    c.max_lines = 3;
    for i in 0..10 {
        c.push_line(format!("line {i}"));
    }
    assert_eq!(c.line_count(), 3, "ring capped to max_lines");
    // The last three lines survive.
    assert!(c.plain_text().contains("line 9"));
    assert!(!c.plain_text().contains("line 0"));
}

#[test]
fn copy_yields_the_plain_scrollback() {
    let mut c = Console::new("term");
    c.push_line("\x1b[31merror:\x1b[0m boom");
    c.push_line("trace ...");
    let copied = c.copy().unwrap();
    assert_eq!(copied, "error: boom\ntrace ...", "ANSI stripped in the copy text");
}

#[test]
fn scrollback_animation_is_deterministic_under_injected_clock() {
    let mut a = Console::new("a");
    let mut b = Console::new("b");
    for i in 0..200 {
        a.push_line(format!("l{i}"));
        b.push_line(format!("l{i}"));
    }
    a.scroll.set_max(1000.0);
    b.scroll.set_max(1000.0);
    a.scroll.scroll_to(0.0);
    b.scroll.scroll_to(0.0);
    a.scroll.scroll_by(300.0);
    b.scroll.scroll_by(300.0);
    for _ in 0..30 {
        a.advance(1.0 / 60.0);
        b.advance(1.0 / 60.0);
    }
    assert!((a.scroll.offset - b.scroll.offset).abs() < 1e-6, "same input → same scroll (FC-7)");
}

#[test]
fn caps_and_serde_round_trip() {
    let mut c = Console::new("term").with_cursor(Cursor::Beam).with_prompt("> ");
    c.push_line("\x1b[36mhi\x1b[0m");
    let caps = c.caps();
    assert!(caps.themeable && caps.copyable && caps.searchable && caps.scalable);
    let json = serde_json::to_string(&c).unwrap();
    let back: Console = serde_json::from_str(&json).unwrap();
    assert_eq!(back.state_json(), c.state_json());
}

#[test]
fn headless_render_draws_a_populated_console() {
    use facett_core::harness::headless_render;
    let mut c = Console::new("term");
    for i in 0..40 {
        c.push_line(format!("\x1b[32m[{i:03}]\x1b[0m output line"));
    }
    let r = headless_render(&mut c);
    assert!(r.drew(), "console renders a non-empty frame headlessly");
    assert_eq!(r.state["lines"], 40);
}

#[test]
fn themed_render_works_for_a_look_preset() {
    use facett_core::harness::render_themed;
    let mut c = Console::new("term");
    c.push_line("ready");
    // Apply the look-preset's *derived* palette through the legacy bridge.
    let legacy = facett_core::look::Theme::macos_dark().to_legacy_palette();
    let r = render_themed(&mut c, legacy);
    assert!(r.drew());
}