#![allow(clippy::uninlined_format_args)]
mod common;
use common::harness::E2EHarness;
use common::mock_terminal::MockTerminal;
use opentui::ansi::AnsiWriter;
use opentui::buffer::{BoxStyle, OptimizedBuffer};
use opentui::grapheme_pool::GraphemePool;
use opentui::renderer::{BufferDiff, Renderer, RendererOptions};
use opentui::style::TextAttributes;
use opentui::{Cell, Rgba, Style};
use opentui_rust as opentui;
use std::io::Write;
#[test]
fn test_e2e_basic_render_cycle() {
let mut harness = E2EHarness::new("render_cycle", "basic_cycle", 40, 10);
harness
.log()
.info("init", "Starting basic render cycle test");
let mut front_buffer = OptimizedBuffer::new(40, 10);
let mut back_buffer = OptimizedBuffer::new(40, 10);
harness.log().info("init", "Buffers initialized: 40x10");
back_buffer.draw_text(0, 0, "Hello, OpenTUI!", Style::fg(Rgba::GREEN));
back_buffer.draw_text(0, 1, "Frame 1", Style::fg(Rgba::WHITE));
harness
.log()
.info("draw", "Drew initial content to back buffer");
let diff1 = BufferDiff::compute(&front_buffer, &back_buffer);
harness.log().info(
"diff",
format!("First frame diff: {} cells changed", diff1.change_count),
);
assert!(diff1.change_count > 0, "First frame should have changes");
assert!(
!diff1.changed_cells.is_empty(),
"Changed cells list should not be empty"
);
let row0_changes = diff1
.changed_cells
.iter()
.filter(|(_, y)| matches!(*y, 0))
.count();
assert!(
row0_changes >= 15,
"Row 0 should have at least 15 changed cells for 'Hello, OpenTUI!'"
);
std::mem::swap(&mut front_buffer, &mut back_buffer);
back_buffer.clear(Rgba::TRANSPARENT);
back_buffer.draw_text(0, 0, "Hello, OpenTUI!", Style::fg(Rgba::GREEN));
back_buffer.draw_text(0, 1, "Frame 2", Style::fg(Rgba::WHITE));
harness
.log()
.info("draw", "Drew Frame 2 content (modified)");
let diff2 = BufferDiff::compute(&front_buffer, &back_buffer);
harness.log().info(
"diff",
format!("Second frame diff: {} cells changed", diff2.change_count),
);
assert!(
diff2.change_count < diff1.change_count,
"Second frame should have fewer changes than first (only 'Frame 1' -> 'Frame 2')"
);
let row0_changes2 = diff2.changed_cells.iter().any(|(_, y)| matches!(*y, 0));
assert!(
!row0_changes2,
"Row 0 should have no changes (content unchanged)"
);
let row1_changes = diff2.changed_cells.iter().any(|(_, y)| matches!(*y, 1));
assert!(
row1_changes,
"Row 1 should have changes (Frame 1 -> Frame 2)"
);
harness.dump_buffer("final_state");
harness.finish(true);
eprintln!("[TEST] PASS: E2E basic render cycle works");
}
#[test]
#[allow(clippy::too_many_lines)] #[allow(clippy::cast_precision_loss)] fn test_e2e_layered_gradient_hit_test() {
let width = 40;
let height = 12;
let mut harness = E2EHarness::new("render_cycle", "layered_gradient_hit_test", width, height);
harness
.log()
.info("init", "Starting layered gradient + hit-test test");
let mut renderer = Renderer::new_with_options(
width,
height,
RendererOptions {
use_alt_screen: false,
hide_cursor: false,
enable_mouse: false,
query_capabilities: false,
},
)
.expect("Renderer creation should succeed with options disabled");
let top = Rgba::from_hex("#1a1a2e").unwrap_or(Rgba::BLACK);
let bottom = Rgba::from_hex("#16213e").unwrap_or(Rgba::BLACK);
harness
.log()
.info("render", "Layer 0: vertical gradient background");
renderer.render_to_layer(0, |buf| {
let denom = height.saturating_sub(1).max(1) as f32;
for y in 0..height {
let t = y as f32 / denom;
let color = top.lerp(bottom, t);
buf.fill_rect(0, y, width, 1, color);
}
});
renderer.register_hit_area(0, 0, width, height, 0);
let panel_x = 6;
let panel_y = 3;
let panel_w = 28;
let panel_h = 6;
let panel_base = Rgba::from_hex("#0f3460")
.unwrap_or(Rgba::BLACK)
.with_alpha(0.85);
harness
.log()
.info("render", "Layer 1: translucent panel + border");
renderer.render_to_layer(1, |buf| {
buf.fill_rect(panel_x, panel_y, panel_w, panel_h, panel_base);
buf.draw_box(
panel_x,
panel_y,
panel_w,
panel_h,
BoxStyle::double(Style::fg(Rgba::WHITE)),
);
buf.draw_text(panel_x + 2, panel_y + 2, "Panel", Style::fg(Rgba::WHITE));
});
renderer.register_hit_area(panel_x, panel_y, panel_w, panel_h, 1);
let stats_before = renderer.stats().clone();
harness
.log()
.info("present", "Merging layers and presenting frame");
renderer.present().expect("present should succeed");
let stats_after = renderer.stats().clone();
harness.log().info(
"stats",
format!(
"frames {} -> {}, last_frame_cells={}",
stats_before.frames, stats_after.frames, stats_after.last_frame_cells
),
);
let bg_hit = renderer.hit_test(1, 1);
let panel_hit = renderer.hit_test(panel_x + 1, panel_y + 1);
harness.log().info(
"hit_test",
format!("bg_hit={:?}, panel_hit={:?}", bg_hit, panel_hit),
);
assert_eq!(bg_hit, Some(0), "Background hit should resolve to layer 0");
assert_eq!(panel_hit, Some(1), "Panel hit should resolve to layer 1");
let front = renderer.front_buffer();
let sample_x = panel_x + 1;
let sample_y = panel_y + 1;
let sample_cell = front
.get(sample_x, sample_y)
.unwrap_or_else(|| unreachable!("No cell at ({}, {})", sample_x, sample_y));
let denom = height.saturating_sub(1).max(1) as f32;
let base_color = top.lerp(bottom, sample_y as f32 / denom);
let expected = panel_base.blend_over(base_color);
let color_close = |a: Rgba, b: Rgba| {
const EPS: f32 = 0.01;
(a.r - b.r).abs() < EPS
&& (a.g - b.g).abs() < EPS
&& (a.b - b.b).abs() < EPS
&& (a.a - b.a).abs() < EPS
};
harness.log().info(
"color",
format!(
"base={:?} overlay={:?} expected={:?} actual={:?}",
base_color, panel_base, expected, sample_cell.bg
),
);
assert!(
color_close(sample_cell.bg, expected),
"Blended panel bg should match expected composite"
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E layered gradient + hit-test works");
}
#[test]
fn test_e2e_pattern_blend_overlay() {
let width = 20;
let height = 8;
let mut harness = E2EHarness::new("render_cycle", "pattern_blend_overlay", width, height);
harness
.log()
.info("init", "Starting pattern blend overlay test");
let mut buffer = OptimizedBuffer::new(width, height);
let dark = Rgba::from_hex("#2d2d2d").unwrap_or(Rgba::BLACK);
let light = Rgba::from_hex("#3a3a3a").unwrap_or(Rgba::BLACK);
for y in 0..height {
for x in 0..width {
let bg = if matches!((x + y) % 2, 0) {
dark
} else {
light
};
buffer.set(x, y, Cell::clear(bg));
}
}
let overlay = Rgba::from_hex("#ff6b6b")
.unwrap_or(Rgba::WHITE)
.with_alpha(0.5);
let overlay_x = 4;
let overlay_y = 2;
let overlay_w = 10;
let overlay_h = 3;
buffer.fill_rect(overlay_x, overlay_y, overlay_w, overlay_h, overlay);
let sample_x = overlay_x + 1;
let sample_y = overlay_y + 1;
let base_bg = if matches!((sample_x + sample_y) % 2, 0) {
dark
} else {
light
};
let expected = overlay.blend_over(base_bg);
let sample = buffer
.get(sample_x, sample_y)
.unwrap_or_else(|| unreachable!("No cell at ({}, {})", sample_x, sample_y));
let color_close = |a: Rgba, b: Rgba| {
const EPS: f32 = 0.01;
(a.r - b.r).abs() < EPS
&& (a.g - b.g).abs() < EPS
&& (a.b - b.b).abs() < EPS
&& (a.a - b.a).abs() < EPS
};
harness.log().info(
"blend",
format!(
"base={:?} overlay={:?} expected={:?} actual={:?}",
base_bg, overlay, expected, sample.bg
),
);
assert!(
color_close(sample.bg, expected),
"Overlay blend should match expected composite"
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E pattern blend overlay works");
}
#[test]
fn test_e2e_first_frame_full_output() {
let mut harness = E2EHarness::new("render_cycle", "first_frame_full", 20, 5);
harness
.log()
.info("init", "Testing first frame full output");
let front_buffer = OptimizedBuffer::new(20, 5);
let mut back_buffer = OptimizedBuffer::new(20, 5);
back_buffer.draw_text(0, 0, "Line 1", Style::default());
back_buffer.draw_text(0, 1, "Line 2", Style::default());
back_buffer.draw_text(0, 2, "Line 3", Style::default());
let diff = BufferDiff::compute(&front_buffer, &back_buffer);
let total_drawn_cells = 6 + 6 + 6; assert!(
diff.change_count >= total_drawn_cells,
"First frame should include all drawn cells: expected >= {}, got {}",
total_drawn_cells,
diff.change_count
);
harness.log().info(
"verify",
format!(
"First frame has {} changes (expected >= {})",
diff.change_count, total_drawn_cells
),
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E first frame full output works");
}
#[test]
fn test_e2e_subsequent_frames_diff_only() {
let mut harness = E2EHarness::new("render_cycle", "diff_only", 30, 5);
harness
.log()
.info("init", "Testing subsequent frames diff-only output");
let mut front_buffer = OptimizedBuffer::new(30, 5);
let mut back_buffer = OptimizedBuffer::new(30, 5);
front_buffer.draw_text(0, 0, "Static content here", Style::default());
front_buffer.draw_text(0, 1, "Counter: 0", Style::default());
back_buffer.draw_text(0, 0, "Static content here", Style::default());
back_buffer.draw_text(0, 1, "Counter: 1", Style::default());
let diff = BufferDiff::compute(&front_buffer, &back_buffer);
harness.log().info(
"diff",
format!("Diff has {} changed cells", diff.change_count),
);
assert!(
diff.change_count <= 3,
"Only counter digit should change: expected <= 3, got {}",
diff.change_count
);
assert!(
!diff.changed_cells.iter().any(|(_, y)| matches!(*y, 0)),
"Static row should have no changes"
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E subsequent frames diff-only works");
}
#[test]
fn test_e2e_force_redraw_full_output() {
let mut harness = E2EHarness::new("render_cycle", "force_redraw", 20, 5);
harness
.log()
.info("init", "Testing force redraw full output");
let mut front_buffer = OptimizedBuffer::new(20, 5);
let mut back_buffer = OptimizedBuffer::new(20, 5);
let content = "Same content";
front_buffer.draw_text(0, 0, content, Style::default());
back_buffer.draw_text(0, 0, content, Style::default());
let diff = BufferDiff::compute(&front_buffer, &back_buffer);
assert_eq!(
diff.change_count, 0,
"Identical buffers should have no diff"
);
harness.log().info("verify", "Identical buffers: no diff");
let total_cells = 20 * 5;
let force_diff = BufferDiff {
changed_cells: (0..20u32)
.flat_map(|x| (0..5u32).map(move |y| (x, y)))
.collect(),
dirty_regions: vec![],
change_count: total_cells,
};
assert_eq!(
force_diff.change_count, total_cells,
"Force redraw should include all {} cells",
total_cells
);
harness.log().info(
"verify",
format!("Force redraw: {} cells", force_diff.change_count),
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E force redraw full output works");
}
#[test]
fn test_e2e_clear_and_draw() {
let mut harness = E2EHarness::new("render_cycle", "clear_draw", 20, 5);
harness.log().info("init", "Testing clear + draw sequence");
let mut front_buffer = OptimizedBuffer::new(20, 5);
front_buffer.draw_text(0, 0, "Old content", Style::default());
let mut back_buffer = OptimizedBuffer::new(20, 5);
back_buffer.clear(Rgba::BLACK);
back_buffer.draw_text(0, 0, "New content", Style::fg(Rgba::RED));
let diff = BufferDiff::compute(&front_buffer, &back_buffer);
harness.log().info(
"diff",
format!("Clear + draw diff: {} cells changed", diff.change_count),
);
assert!(diff.change_count > 0, "Clear + draw should produce changes");
assert!(
diff.changed_cells.iter().any(|(_, y)| matches!(*y, 0)),
"Row 0 should have changes after clear + draw"
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E clear and draw works");
}
#[test]
fn test_e2e_ansi_cursor_positioning() {
let mut harness = E2EHarness::new("render_cycle", "cursor_positioning", 40, 10);
harness
.log()
.info("init", "Testing ANSI cursor positioning");
let mut output: Vec<u8> = Vec::new();
{
let mut writer = AnsiWriter::new(&mut output);
writer.move_cursor(0, 0);
writer.move_cursor(5, 10);
writer.move_cursor(9, 39);
writer.flush().unwrap();
}
let output_str = String::from_utf8_lossy(&output);
harness
.log()
.info("ansi", format!("Output length: {} bytes", output.len()));
let mut mock = MockTerminal::new(40, 10);
mock.write_all(&output).unwrap();
let cursor_moves = mock.cursor_moves();
harness
.log()
.info("verify", format!("Cursor moves: {:?}", cursor_moves));
assert!(
output_str.contains("\x1b["),
"Output should contain CSI sequences"
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E ANSI cursor positioning works");
}
#[test]
fn test_e2e_ansi_color_sequences() {
let mut harness = E2EHarness::new("render_cycle", "color_sequences", 40, 10);
harness.log().info("init", "Testing ANSI color sequences");
let style = Style::builder().fg(Rgba::RED).bg(Rgba::BLUE).build();
let cell = opentui::cell::Cell::new('X', style);
let mut output: Vec<u8> = Vec::new();
{
let mut writer = AnsiWriter::new(&mut output);
writer.move_cursor(0, 0);
writer.write_cell(&cell);
writer.flush().unwrap();
}
let output_str = String::from_utf8_lossy(&output);
harness.log().info(
"ansi",
format!("Output: {}", output_str.replace('\x1b', "ESC")),
);
assert!(
output_str.contains("\x1b[38;2;255;0;0m"),
"Output should contain red foreground color sequence"
);
assert!(
output_str.contains("\x1b[48;2;0;0;255m"),
"Output should contain blue background color sequence"
);
assert!(
output_str.contains('X'),
"Output should contain the cell character"
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E ANSI color sequences work");
}
#[test]
fn test_e2e_ansi_text_attributes() {
let mut harness = E2EHarness::new("render_cycle", "text_attributes", 40, 10);
harness
.log()
.info("init", "Testing ANSI text attribute sequences");
let bold_style = Style::bold();
let italic_style = Style::italic();
let underline_style = Style::underline();
let bold_cell = opentui::cell::Cell::new('B', bold_style);
let italic_cell = opentui::cell::Cell::new('I', italic_style);
let underline_cell = opentui::cell::Cell::new('U', underline_style);
let mut output: Vec<u8> = Vec::new();
{
let mut writer = AnsiWriter::new(&mut output);
writer.move_cursor(0, 0);
writer.write_cell(&bold_cell);
writer.reset();
writer.move_cursor(0, 1);
writer.write_cell(&italic_cell);
writer.reset();
writer.move_cursor(0, 2);
writer.write_cell(&underline_cell);
writer.flush().unwrap();
}
let output_str = String::from_utf8_lossy(&output);
let readable = output_str.replace('\x1b', "ESC");
harness.log().info("ansi", format!("Output: {readable}"));
let mut mock = MockTerminal::new(40, 10);
mock.write_all(&output).unwrap();
let sequences = mock.parse_sequences();
let has_bold = sequences
.iter()
.any(|s| matches!(s, common::mock_terminal::AnsiSequence::SetAttributes(a) if a.contains(TextAttributes::BOLD)));
let has_italic = sequences
.iter()
.any(|s| matches!(s, common::mock_terminal::AnsiSequence::SetAttributes(a) if a.contains(TextAttributes::ITALIC)));
let has_underline = sequences
.iter()
.any(|s| matches!(s, common::mock_terminal::AnsiSequence::SetAttributes(a) if a.contains(TextAttributes::UNDERLINE)));
harness.log().info(
"verify",
format!(
"Attributes found: bold={}, italic={}, underline={}",
has_bold, has_italic, has_underline
),
);
assert!(
output_str.contains("\x1b[1m"),
"Output should contain bold sequence (CSI 1 m)"
);
assert!(
output_str.contains("\x1b[3m"),
"Output should contain italic sequence (CSI 3 m)"
);
assert!(
output_str.contains("\x1b[4m"),
"Output should contain underline sequence (CSI 4 m)"
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E ANSI text attributes work");
}
#[test]
fn test_e2e_ansi_reset_on_cleanup() {
let mut harness = E2EHarness::new("render_cycle", "reset_cleanup", 40, 10);
harness.log().info("init", "Testing ANSI reset on cleanup");
let mut output: Vec<u8> = Vec::new();
{
let mut writer = AnsiWriter::new(&mut output);
let styled = Style::builder().fg(Rgba::RED).bold().underline().build();
let styled_cell = opentui::cell::Cell::new('S', styled);
writer.write_cell(&styled_cell);
writer.reset();
writer.flush().unwrap();
}
let output_str = String::from_utf8_lossy(&output);
harness.log().info(
"ansi",
format!("Output: {}", output_str.replace('\x1b', "ESC")),
);
assert!(
output_str.contains("\x1b[0m") || output_str.contains("\x1b[m"),
"Output should contain reset sequence"
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E ANSI reset on cleanup works");
}
#[test]
fn test_e2e_render_cycle_with_graphemes() {
let mut harness = E2EHarness::new("render_cycle", "graphemes", 40, 10);
harness
.log()
.info("init", "Testing render cycle with graphemes");
let mut grapheme_pool = GraphemePool::new();
let emoji_id = grapheme_pool.alloc("🎉");
let family_id = grapheme_pool.alloc("👨👩👧");
harness.log().info(
"pool",
format!("Allocated grapheme IDs: emoji={emoji_id:?}, family={family_id:?}"),
);
let mut buffer = OptimizedBuffer::new(40, 10);
buffer.draw_text(0, 0, "Party: 🎉", Style::default());
assert_eq!(
grapheme_pool.get(emoji_id),
Some("🎉"),
"Should retrieve emoji from pool"
);
assert_eq!(
grapheme_pool.get(family_id),
Some("👨👩👧"),
"Should retrieve family emoji from pool"
);
let mut output: Vec<u8> = Vec::new();
{
let mut writer = AnsiWriter::new(&mut output);
for y in 0..1u32 {
for x in 0..20u32 {
if let Some(cell) = buffer.get(x, y) {
if !cell.is_continuation() {
writer.write_cell_with_pool(cell, &grapheme_pool);
}
}
}
}
writer.flush().unwrap();
}
let output_str = String::from_utf8_lossy(&output);
harness
.log()
.info("ansi", format!("Output length: {} bytes", output.len()));
assert!(
output_str.contains("Party"),
"Output should contain 'Party'"
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E render cycle with graphemes works");
}
#[test]
fn test_e2e_diff_threshold_decision() {
let mut harness = E2EHarness::new("render_cycle", "diff_threshold", 20, 10);
harness
.log()
.info("init", "Testing diff threshold for full redraw");
let total_cells = 20 * 10;
let small_diff = BufferDiff {
changed_cells: vec![(0, 0); total_cells / 10],
dirty_regions: vec![],
change_count: total_cells / 10,
};
assert!(
!small_diff.should_full_redraw(total_cells),
"10% changes should use diff rendering"
);
harness.log().info("threshold", "10% changes: diff mode");
let large_diff = BufferDiff {
changed_cells: vec![(0, 0); total_cells * 6 / 10],
dirty_regions: vec![],
change_count: total_cells * 6 / 10,
};
assert!(
large_diff.should_full_redraw(total_cells),
"60% changes should trigger full redraw"
);
harness.log().info("threshold", "60% changes: full redraw");
let half_diff = BufferDiff {
changed_cells: vec![(0, 0); total_cells / 2],
dirty_regions: vec![],
change_count: total_cells / 2,
};
harness.log().info(
"threshold",
format!(
"50% changes: full_redraw={}",
half_diff.should_full_redraw(total_cells)
),
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E diff threshold decision works");
}
#[test]
fn test_e2e_render_cycle_logging() {
#[derive(serde::Serialize)]
struct RenderStats {
frame: u32,
changed_cells: usize,
bytes_output: usize,
}
let mut harness = E2EHarness::new("render_cycle", "logging", 20, 5);
harness.log().info("step", "init");
harness.log().info("step", "draw");
harness.log().info("step", "present");
harness.log().info("step", "verify");
harness.log().info("step", "cleanup");
let stats = RenderStats {
frame: 1,
changed_cells: 50,
bytes_output: 256,
};
harness.log().info(
"present",
format!(
"Frame {}: {} cells, {} bytes",
stats.frame, stats.changed_cells, stats.bytes_output
),
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E render cycle logging works");
}