use crate::transcript::{Block, Entry, EntryKind, Transcript, render};
fn input() -> egui::RawInput {
egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(1024.0, 4096.0),
)),
..Default::default()
}
}
fn rendered_text(t: &Transcript, raw: bool) -> String {
let ctx = egui::Context::default();
let output = ctx.run(input(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| render(ui, t, raw));
});
let mut out = String::new();
for clipped in output.shapes {
collect_text(&clipped.shape, &mut out);
}
out
}
fn collect_text(shape: &egui::Shape, out: &mut String) {
match shape {
egui::Shape::Text(t) => {
out.push_str(t.galley.text());
out.push('\n');
}
egui::Shape::Vec(shapes) => {
for s in shapes {
collect_text(s, out);
}
}
_ => {}
}
}
fn repaint_delay(t: &Transcript) -> std::time::Duration {
let ctx = egui::Context::default();
for _ in 0..2 {
let _ = ctx.run(input(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| render(ui, t, false));
});
}
ctx.run(input(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| render(ui, t, false));
})
.viewport_output
.get(&egui::ViewportId::ROOT)
.expect("root viewport output present")
.repaint_delay
}
fn entry(kind: EntryKind) -> Entry {
Entry {
name: "001-x.json".into(),
raw: b"RAWBYTES".to_vec(),
kind,
}
}
fn tx(entries: Vec<Entry>) -> Transcript {
Transcript { entries }
}
#[test]
fn empty_transcript_shows_placeholder() {
assert!(rendered_text(&Transcript::default(), false).contains("(no messages yet)"));
}
#[test]
fn delivered_renders_sender_and_body() {
let t = tx(vec![entry(EntryKind::Delivered {
sender: "alice".into(),
body: "hello world".into(),
})]);
let painted = rendered_text(&t, false);
assert!(painted.contains("alice:"), "got:\n{painted}");
assert!(painted.contains("hello world"));
}
#[test]
fn model_text_and_thinking_render() {
let t = tx(vec![entry(EntryKind::Model {
model_id: "opus".into(),
blocks: vec![
Block::Text("visible answer".into()),
Block::Thinking("private reasoning".into()),
],
})]);
let painted = rendered_text(&t, false);
assert!(painted.contains("opus"));
assert!(painted.contains("visible answer"));
assert!(
painted.contains("thinking: private reasoning"),
"got:\n{painted}"
);
}
fn tool_use_entry() -> Entry {
entry(EntryKind::Model {
model_id: "opus".into(),
blocks: vec![Block::ToolUse {
id: "toolu_9".into(),
name: "Read".into(),
input_summary: r#"{"path":"/x"}"#.into(),
}],
})
}
#[test]
fn in_progress_tool_chip_pulses_and_paints() {
let t = tx(vec![tool_use_entry()]);
assert!(
repaint_delay(&t) < std::time::Duration::from_secs(1),
"in-progress tool must schedule a near-term repaint"
);
let painted = rendered_text(&t, false);
assert!(
painted.contains(r#"Read({"path":"/x"})"#),
"got:\n{painted}"
);
assert!(painted.contains("(in-flight)"));
assert!(painted.contains("toolu_9"));
}
#[test]
fn resolved_tool_chip_is_static() {
let t = tx(vec![
tool_use_entry(),
entry(EntryKind::ToolResult {
tool_use_id: "toolu_9".into(),
content: "file body".into(),
is_error: false,
}),
]);
assert_eq!(
repaint_delay(&t),
std::time::Duration::MAX,
"resolved tools must not pull repaints"
);
let painted = rendered_text(&t, false);
assert!(painted.contains(r#"Read({"path":"/x"})"#));
assert!(!painted.contains("(in-flight)"));
}
#[test]
fn tool_result_renders_content_and_error_glyph() {
let ok = tx(vec![entry(EntryKind::ToolResult {
tool_use_id: "t".into(),
content: "all good".into(),
is_error: false,
})]);
let ok_painted = rendered_text(&ok, false);
assert!(ok_painted.contains("all good"));
assert!(ok_painted.contains("✓"));
let err = tx(vec![entry(EntryKind::ToolResult {
tool_use_id: "t".into(),
content: "boom".into(),
is_error: true,
})]);
let err_painted = rendered_text(&err, false);
assert!(err_painted.contains("boom"));
assert!(err_painted.contains("✗"));
}
#[test]
fn streaming_tail_renders_live_marker() {
let t = tx(vec![entry(EntryKind::Streaming {
text: "partial output".into(),
})]);
let painted = rendered_text(&t, false);
assert!(painted.contains("live"));
assert!(painted.contains("partial output"));
}
#[test]
fn raw_kind_renders_bytes_in_parsed_mode() {
let painted = rendered_text(&tx(vec![entry(EntryKind::Raw)]), false);
assert!(painted.contains("RAWBYTES"), "got:\n{painted}");
assert!(painted.contains("001-x.json"));
}
#[test]
fn raw_toggle_shows_verbatim_bytes_over_parsed() {
let t = tx(vec![entry(EntryKind::Model {
model_id: "opus".into(),
blocks: vec![Block::Text("parsed answer".into())],
})]);
let parsed = rendered_text(&t, false);
assert!(parsed.contains("parsed answer"));
assert!(!parsed.contains("RAWBYTES"));
let raw = rendered_text(&t, true);
assert!(raw.contains("RAWBYTES"), "got:\n{raw}");
assert!(raw.contains("001-x.json"));
assert!(!raw.contains("parsed answer"));
}
#[test]
fn collect_text_recurses_into_shape_vec() {
use egui::{Color32, FontId, Pos2, Stroke};
let ctx = egui::Context::default();
let mut nested: Option<egui::Shape> = None;
let mut flat: Option<egui::Shape> = None;
let _ = ctx.run(egui::RawInput::default(), |ctx| {
let inner = ctx
.fonts(|f| f.layout_no_wrap("nested-text".into(), FontId::default(), Color32::WHITE));
let outer =
ctx.fonts(|f| f.layout_no_wrap("flat-text".into(), FontId::default(), Color32::WHITE));
let shape = |galley| {
egui::Shape::Text(egui::epaint::TextShape {
pos: Pos2::ZERO,
galley,
underline: Stroke::NONE,
fallback_color: Color32::WHITE,
override_text_color: None,
opacity_factor: 1.0,
angle: 0.0,
})
};
nested = Some(egui::Shape::Vec(vec![shape(inner)]));
flat = Some(shape(outer));
});
let mut out = String::new();
collect_text(nested.as_ref().unwrap(), &mut out);
collect_text(flat.as_ref().unwrap(), &mut out);
collect_text(&egui::Shape::Noop, &mut out);
assert!(out.contains("nested-text"));
assert!(out.contains("flat-text"));
}