1#![allow(dead_code, unreachable_pub)]
32
33use crate::color::ColorMode;
34use crate::direct::{CellBuffer, DiffRenderer, DirectTerminalCanvas};
35use crate::error::{TuiError, VerificationError};
36use crate::input::InputHandler;
37use crossterm::{
38 cursor,
39 event::{self, Event as CrosstermEvent, KeyCode},
40 execute,
41 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
42};
43use presentar_core::{Constraints, Rect, Widget};
44use std::io::{self, Stdout, Write};
45use std::time::{Duration, Instant};
46
47pub trait Snapshot: Clone + Send + 'static {
61 fn empty() -> Self;
63}
64
65pub trait AsyncCollector: Send + 'static {
91 type Snapshot: Snapshot;
93
94 fn collect(&mut self) -> Self::Snapshot;
99}
100
101pub trait SnapshotReceiver {
119 type Snapshot: Snapshot;
121
122 fn apply_snapshot(&mut self, snapshot: Self::Snapshot);
127}
128
129#[derive(Debug, Clone, Default)]
133pub struct QaTimings {
134 pub input_times_us: Vec<u64>,
136 pub lock_times_us: Vec<u64>,
138 pub render_times_us: Vec<u64>,
140 pub last_collect_us: u64,
142}
143
144impl QaTimings {
145 #[must_use]
147 pub fn new() -> Self {
148 Self::default()
149 }
150
151 pub fn record_input(&mut self, duration: Duration) {
153 self.input_times_us.push(duration.as_micros() as u64);
154 }
155
156 pub fn record_lock(&mut self, duration: Duration) {
158 self.lock_times_us.push(duration.as_micros() as u64);
159 }
160
161 pub fn record_render(&mut self, duration: Duration) {
163 self.render_times_us.push(duration.as_micros() as u64);
164 }
165
166 #[must_use]
168 pub fn format_report(&self) -> String {
169 let avg = |v: &[u64]| {
170 if v.is_empty() {
171 0
172 } else {
173 v.iter().sum::<u64>() / v.len() as u64
174 }
175 };
176 let max = |v: &[u64]| v.iter().max().copied().unwrap_or(0);
177
178 format!(
179 "[QA] input: avg={}us max={}us | lock: avg={}us max={}us | render: avg={}us max={}us | collect: {}us",
180 avg(&self.input_times_us), max(&self.input_times_us),
181 avg(&self.lock_times_us), max(&self.lock_times_us),
182 avg(&self.render_times_us), max(&self.render_times_us),
183 self.last_collect_us
184 )
185 }
186
187 pub fn clear(&mut self) {
189 self.input_times_us.clear();
190 self.lock_times_us.clear();
191 self.render_times_us.clear();
192 }
193}
194
195pub trait Terminal {
201 fn enter(&mut self) -> Result<(), TuiError>;
203 fn leave(&mut self) -> Result<(), TuiError>;
205 fn size(&self) -> Result<(u16, u16), TuiError>;
207 fn poll(&self, timeout: Duration) -> Result<bool, TuiError>;
209 fn read_event(&self) -> Result<CrosstermEvent, TuiError>;
211 fn flush(
213 &mut self,
214 buffer: &mut CellBuffer,
215 renderer: &mut DiffRenderer,
216 ) -> Result<(), TuiError>;
217 fn enable_mouse(&mut self) -> Result<(), TuiError>;
219 fn disable_mouse(&mut self) -> Result<(), TuiError>;
221}
222
223pub trait TerminalBackend {
226 fn enable_raw_mode(&mut self) -> Result<(), TuiError>;
227 fn disable_raw_mode(&mut self) -> Result<(), TuiError>;
228 fn enter_alternate_screen(&mut self) -> Result<(), TuiError>;
229 fn leave_alternate_screen(&mut self) -> Result<(), TuiError>;
230 fn hide_cursor(&mut self) -> Result<(), TuiError>;
231 fn show_cursor(&mut self) -> Result<(), TuiError>;
232 fn size(&self) -> Result<(u16, u16), TuiError>;
233 fn poll(&self, timeout: Duration) -> Result<bool, TuiError>;
234 fn read_event(&self) -> Result<CrosstermEvent, TuiError>;
235 fn write_flush(
236 &mut self,
237 buffer: &mut CellBuffer,
238 renderer: &mut DiffRenderer,
239 ) -> Result<(), TuiError>;
240 fn enable_mouse_capture(&mut self) -> Result<(), TuiError>;
241 fn disable_mouse_capture(&mut self) -> Result<(), TuiError>;
242}
243
244pub struct CrosstermBackend {
246 stdout: Stdout,
247}
248
249impl CrosstermBackend {
250 pub fn new() -> Self {
251 Self {
252 stdout: io::stdout(),
253 }
254 }
255}
256
257impl Default for CrosstermBackend {
258 fn default() -> Self {
259 Self::new()
260 }
261}
262
263impl TerminalBackend for CrosstermBackend {
264 fn enable_raw_mode(&mut self) -> Result<(), TuiError> {
265 enable_raw_mode()?;
266 Ok(())
267 }
268 fn disable_raw_mode(&mut self) -> Result<(), TuiError> {
269 let _ = disable_raw_mode();
270 Ok(())
271 }
272 fn enter_alternate_screen(&mut self) -> Result<(), TuiError> {
273 execute!(self.stdout, EnterAlternateScreen)?;
274 Ok(())
275 }
276 fn leave_alternate_screen(&mut self) -> Result<(), TuiError> {
277 let _ = execute!(self.stdout, LeaveAlternateScreen);
278 Ok(())
279 }
280 fn hide_cursor(&mut self) -> Result<(), TuiError> {
281 execute!(self.stdout, cursor::Hide)?;
282 Ok(())
283 }
284 fn show_cursor(&mut self) -> Result<(), TuiError> {
285 let _ = execute!(self.stdout, cursor::Show);
286 Ok(())
287 }
288 fn size(&self) -> Result<(u16, u16), TuiError> {
289 Ok(crossterm::terminal::size()?)
290 }
291 fn poll(&self, timeout: Duration) -> Result<bool, TuiError> {
292 Ok(event::poll(timeout)?)
293 }
294 fn read_event(&self) -> Result<CrosstermEvent, TuiError> {
295 Ok(event::read()?)
296 }
297 fn write_flush(
298 &mut self,
299 buffer: &mut CellBuffer,
300 renderer: &mut DiffRenderer,
301 ) -> Result<(), TuiError> {
302 renderer.flush(buffer, &mut self.stdout)?;
303 self.stdout.flush()?;
304 Ok(())
305 }
306 fn enable_mouse_capture(&mut self) -> Result<(), TuiError> {
307 execute!(self.stdout, crossterm::event::EnableMouseCapture)?;
308 Ok(())
309 }
310 fn disable_mouse_capture(&mut self) -> Result<(), TuiError> {
311 let _ = execute!(self.stdout, crossterm::event::DisableMouseCapture);
312 Ok(())
313 }
314}
315
316#[allow(clippy::struct_excessive_bools)]
319pub struct TestableBackend<W: Write> {
320 writer: W,
321 size: (u16, u16),
322 raw_mode: bool,
323 alternate_screen: bool,
324 cursor_hidden: bool,
325 mouse_captured: bool,
326 events: std::cell::RefCell<std::collections::VecDeque<CrosstermEvent>>,
327 poll_results: std::cell::RefCell<std::collections::VecDeque<bool>>,
328}
329
330impl<W: Write> TestableBackend<W> {
331 pub fn new(writer: W, width: u16, height: u16) -> Self {
333 Self {
334 writer,
335 size: (width, height),
336 raw_mode: false,
337 alternate_screen: false,
338 cursor_hidden: false,
339 mouse_captured: false,
340 events: std::cell::RefCell::new(std::collections::VecDeque::new()),
341 poll_results: std::cell::RefCell::new(std::collections::VecDeque::new()),
342 }
343 }
344
345 pub fn with_events(self, events: Vec<CrosstermEvent>) -> Self {
347 *self.events.borrow_mut() = events.into_iter().collect();
348 self
349 }
350
351 pub fn with_polls(self, polls: Vec<bool>) -> Self {
353 *self.poll_results.borrow_mut() = polls.into_iter().collect();
354 self
355 }
356
357 pub fn is_raw_mode(&self) -> bool {
359 self.raw_mode
360 }
361
362 pub fn is_alternate_screen(&self) -> bool {
364 self.alternate_screen
365 }
366
367 pub fn is_cursor_hidden(&self) -> bool {
369 self.cursor_hidden
370 }
371
372 pub fn is_mouse_captured(&self) -> bool {
374 self.mouse_captured
375 }
376
377 pub fn into_writer(self) -> W {
379 self.writer
380 }
381}
382
383impl<W: Write> TerminalBackend for TestableBackend<W> {
384 fn enable_raw_mode(&mut self) -> Result<(), TuiError> {
385 self.raw_mode = true;
386 Ok(())
387 }
388
389 fn disable_raw_mode(&mut self) -> Result<(), TuiError> {
390 self.raw_mode = false;
391 Ok(())
392 }
393
394 fn enter_alternate_screen(&mut self) -> Result<(), TuiError> {
395 self.alternate_screen = true;
396 execute!(self.writer, EnterAlternateScreen)?;
398 Ok(())
399 }
400
401 fn leave_alternate_screen(&mut self) -> Result<(), TuiError> {
402 self.alternate_screen = false;
403 let _ = execute!(self.writer, LeaveAlternateScreen);
404 Ok(())
405 }
406
407 fn hide_cursor(&mut self) -> Result<(), TuiError> {
408 self.cursor_hidden = true;
409 execute!(self.writer, cursor::Hide)?;
410 Ok(())
411 }
412
413 fn show_cursor(&mut self) -> Result<(), TuiError> {
414 self.cursor_hidden = false;
415 let _ = execute!(self.writer, cursor::Show);
416 Ok(())
417 }
418
419 fn size(&self) -> Result<(u16, u16), TuiError> {
420 Ok(self.size)
421 }
422
423 fn poll(&self, _timeout: Duration) -> Result<bool, TuiError> {
424 Ok(self.poll_results.borrow_mut().pop_front().unwrap_or(false))
425 }
426
427 fn read_event(&self) -> Result<CrosstermEvent, TuiError> {
428 self.events
429 .borrow_mut()
430 .pop_front()
431 .ok_or_else(|| TuiError::Io(io::Error::new(io::ErrorKind::WouldBlock, "no events")))
432 }
433
434 fn write_flush(
435 &mut self,
436 buffer: &mut CellBuffer,
437 renderer: &mut DiffRenderer,
438 ) -> Result<(), TuiError> {
439 renderer.flush(buffer, &mut self.writer)?;
440 self.writer.flush()?;
441 Ok(())
442 }
443
444 fn enable_mouse_capture(&mut self) -> Result<(), TuiError> {
445 self.mouse_captured = true;
446 execute!(self.writer, crossterm::event::EnableMouseCapture)?;
447 Ok(())
448 }
449
450 fn disable_mouse_capture(&mut self) -> Result<(), TuiError> {
451 self.mouse_captured = false;
452 let _ = execute!(self.writer, crossterm::event::DisableMouseCapture);
453 Ok(())
454 }
455}
456
457pub struct GenericTerminal<B: TerminalBackend> {
459 backend: B,
460}
461
462impl<B: TerminalBackend> GenericTerminal<B> {
463 pub fn new(backend: B) -> Self {
464 Self { backend }
465 }
466}
467
468impl<B: TerminalBackend> Terminal for GenericTerminal<B> {
469 fn enter(&mut self) -> Result<(), TuiError> {
470 self.backend.enable_raw_mode()?;
471 self.backend.enter_alternate_screen()?;
472 self.backend.hide_cursor()?;
473 Ok(())
474 }
475
476 fn leave(&mut self) -> Result<(), TuiError> {
477 self.backend.show_cursor()?;
478 self.backend.leave_alternate_screen()?;
479 self.backend.disable_raw_mode()?;
480 Ok(())
481 }
482
483 fn size(&self) -> Result<(u16, u16), TuiError> {
484 self.backend.size()
485 }
486
487 fn poll(&self, timeout: Duration) -> Result<bool, TuiError> {
488 self.backend.poll(timeout)
489 }
490
491 fn read_event(&self) -> Result<CrosstermEvent, TuiError> {
492 self.backend.read_event()
493 }
494
495 fn flush(
496 &mut self,
497 buffer: &mut CellBuffer,
498 renderer: &mut DiffRenderer,
499 ) -> Result<(), TuiError> {
500 self.backend.write_flush(buffer, renderer)
501 }
502
503 fn enable_mouse(&mut self) -> Result<(), TuiError> {
504 self.backend.enable_mouse_capture()
505 }
506
507 fn disable_mouse(&mut self) -> Result<(), TuiError> {
508 self.backend.disable_mouse_capture()
509 }
510}
511
512pub type CrosstermTerminal = GenericTerminal<CrosstermBackend>;
514
515#[derive(Debug, Clone)]
517pub struct TuiConfig {
518 pub tick_rate_ms: u64,
520 pub enable_mouse: bool,
522 pub color_mode: Option<ColorMode>,
524 pub skip_verification: bool,
526 pub target_fps: u32,
528}
529
530impl Default for TuiConfig {
531 fn default() -> Self {
532 Self {
533 tick_rate_ms: 250,
534 enable_mouse: false,
535 color_mode: None,
536 target_fps: 60,
537 skip_verification: false,
538 }
539 }
540}
541
542impl TuiConfig {
543 #[must_use]
545 pub fn high_performance() -> Self {
546 Self {
547 tick_rate_ms: 16,
548 target_fps: 60,
549 ..Default::default()
550 }
551 }
552
553 #[must_use]
555 pub fn power_saving() -> Self {
556 Self {
557 tick_rate_ms: 100,
558 target_fps: 30,
559 ..Default::default()
560 }
561 }
562}
563
564#[derive(Debug, Clone, Default)]
566pub struct FrameMetrics {
567 pub verify_time: Duration,
569 pub measure_time: Duration,
571 pub layout_time: Duration,
573 pub paint_time: Duration,
575 pub total_time: Duration,
577 pub frame_count: u64,
579}
580
581pub struct TuiApp<W: Widget> {
583 root: W,
584 config: TuiConfig,
585 input_handler: InputHandler,
586 metrics: FrameMetrics,
587 should_quit: bool,
588 color_mode: ColorMode,
589}
590
591struct AppRunner<'a, W: Widget, T: Terminal> {
593 app: &'a mut TuiApp<W>,
594 terminal: T,
595 buffer: CellBuffer,
596 renderer: DiffRenderer,
597}
598
599impl<W: Widget, T: Terminal> AppRunner<'_, W, T> {
600 fn run_loop(&mut self) -> Result<(), TuiError> {
601 let tick_duration = Duration::from_millis(self.app.config.tick_rate_ms);
602
603 loop {
604 let frame_start = Instant::now();
605
606 let (width, height) = self.terminal.size()?;
608 if width != self.buffer.width() || height != self.buffer.height() {
609 self.buffer.resize(width, height);
610 self.renderer.reset();
611 }
612
613 let verify_start = Instant::now();
615 if !self.app.config.skip_verification {
616 let verification = self.app.root.verify();
617 if !verification.is_valid() {
618 return Err(TuiError::VerificationFailed(VerificationError::from(
619 verification,
620 )));
621 }
622 }
623 self.app.metrics.verify_time = verify_start.elapsed();
624
625 self.app.render_frame(&mut self.buffer);
627
628 self.terminal.flush(&mut self.buffer, &mut self.renderer)?;
630
631 self.app.metrics.total_time = frame_start.elapsed();
632 self.app.metrics.frame_count += 1;
633
634 if self.terminal.poll(tick_duration)? {
636 if let CrosstermEvent::Key(key) = self.terminal.read_event()? {
637 if key.code == KeyCode::Char('q')
638 || key.code == KeyCode::Char('c')
639 && key
640 .modifiers
641 .contains(crossterm::event::KeyModifiers::CONTROL)
642 {
643 self.app.should_quit = true;
644 }
645
646 if let Some(event) = self.app.input_handler.convert(CrosstermEvent::Key(key)) {
647 let _ = self.app.root.event(&event);
648 }
649 }
650 }
651
652 if self.app.should_quit {
653 break;
654 }
655 }
656
657 Ok(())
658 }
659}
660
661impl<W: Widget> TuiApp<W> {
662 pub fn new(root: W) -> Result<Self, TuiError> {
664 if root.assertions().is_empty() {
666 return Err(TuiError::InvalidBrick(
667 "Root widget has no assertions - every Brick must have at least one falsifiable assertion".to_string(),
668 ));
669 }
670
671 Ok(Self {
672 root,
673 config: TuiConfig::default(),
674 input_handler: InputHandler::new(),
675 metrics: FrameMetrics::default(),
676 should_quit: false,
677 color_mode: ColorMode::detect(),
678 })
679 }
680
681 #[must_use]
683 pub fn with_config(mut self, config: TuiConfig) -> Self {
684 if let Some(mode) = config.color_mode {
685 self.color_mode = mode;
686 }
687 self.config = config;
688 self
689 }
690
691 #[must_use]
693 pub fn with_input_handler(mut self, handler: InputHandler) -> Self {
694 self.input_handler = handler;
695 self
696 }
697
698 #[must_use]
700 pub fn root(&self) -> &W {
701 &self.root
702 }
703
704 pub fn root_mut(&mut self) -> &mut W {
706 &mut self.root
707 }
708
709 #[must_use]
711 pub fn metrics(&self) -> &FrameMetrics {
712 &self.metrics
713 }
714
715 pub fn quit(&mut self) {
717 self.should_quit = true;
718 }
719
720 pub fn run(&mut self) -> Result<(), TuiError> {
722 let backend = CrosstermBackend::new();
723 let terminal = GenericTerminal::new(backend);
724 self.run_with_terminal(terminal)
725 }
726
727 pub fn run_with_terminal<T: Terminal>(&mut self, mut terminal: T) -> Result<(), TuiError> {
730 terminal.enter()?;
731
732 if self.config.enable_mouse {
733 terminal.enable_mouse()?;
734 }
735
736 let (width, height) = terminal.size()?;
738 let buffer = CellBuffer::new(width, height);
739 let renderer = DiffRenderer::with_color_mode(self.color_mode);
740
741 let mut runner = AppRunner {
742 app: self,
743 terminal,
744 buffer,
745 renderer,
746 };
747
748 let result = runner.run_loop();
749
750 if runner.app.config.enable_mouse {
751 runner.terminal.disable_mouse()?;
752 }
753 runner.terminal.leave()?;
754
755 result
756 }
757
758 fn render_frame(&mut self, buffer: &mut CellBuffer) {
759 let width = buffer.width();
760 let height = buffer.height();
761
762 let measure_start = Instant::now();
764 let constraints = Constraints::new(0.0, f32::from(width), 0.0, f32::from(height));
765 let _size = self.root.measure(constraints);
766 self.metrics.measure_time = measure_start.elapsed();
767
768 let layout_start = Instant::now();
770 let bounds = Rect::new(0.0, 0.0, f32::from(width), f32::from(height));
771 let _ = self.root.layout(bounds);
772 self.metrics.layout_time = layout_start.elapsed();
773
774 let paint_start = Instant::now();
776 {
777 let mut canvas = DirectTerminalCanvas::new(buffer);
778 self.root.paint(&mut canvas);
779 }
780 self.metrics.paint_time = paint_start.elapsed();
781 }
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787 use presentar_core::{
788 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Event, LayoutResult,
789 Size, TypeId,
790 };
791 use std::any::Any;
792 use std::time::Duration;
793
794 struct TestWidget {
795 assertions: Vec<BrickAssertion>,
796 }
797
798 impl TestWidget {
799 fn new() -> Self {
800 Self {
801 assertions: vec![BrickAssertion::max_latency_ms(16)],
802 }
803 }
804
805 fn without_assertions() -> Self {
806 Self { assertions: vec![] }
807 }
808 }
809
810 impl Brick for TestWidget {
811 fn brick_name(&self) -> &'static str {
812 "test_widget"
813 }
814
815 fn assertions(&self) -> &[BrickAssertion] {
816 &self.assertions
817 }
818
819 fn budget(&self) -> BrickBudget {
820 BrickBudget::default()
821 }
822
823 fn verify(&self) -> BrickVerification {
824 BrickVerification {
825 passed: self.assertions.clone(),
826 failed: vec![],
827 verification_time: Duration::from_micros(10),
828 }
829 }
830
831 fn to_html(&self) -> String {
832 String::new()
833 }
834
835 fn to_css(&self) -> String {
836 String::new()
837 }
838 }
839
840 impl Widget for TestWidget {
841 fn type_id(&self) -> TypeId {
842 TypeId::of::<Self>()
843 }
844
845 fn measure(&self, constraints: Constraints) -> Size {
846 constraints.constrain(Size::new(10.0, 5.0))
847 }
848
849 fn layout(&mut self, bounds: Rect) -> LayoutResult {
850 LayoutResult {
851 size: Size::new(bounds.width, bounds.height),
852 }
853 }
854
855 fn paint(&self, canvas: &mut dyn Canvas) {
856 canvas.fill_rect(Rect::new(0.0, 0.0, 10.0, 5.0), Color::BLUE);
857 }
858
859 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
860 None
861 }
862
863 fn children(&self) -> &[Box<dyn Widget>] {
864 &[]
865 }
866
867 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
868 &mut []
869 }
870 }
871
872 #[test]
873 fn test_tui_app_creation() {
874 let widget = TestWidget::new();
875 let app = TuiApp::new(widget);
876 assert!(app.is_ok());
877 }
878
879 #[test]
880 fn test_tui_app_rejects_empty_assertions() {
881 let widget = TestWidget::without_assertions();
882 let app = TuiApp::new(widget);
883 assert!(app.is_err());
884 let err = app.err().expect("expected error");
885 assert!(matches!(err, TuiError::InvalidBrick(_)));
886 }
887
888 #[test]
889 fn test_config_default() {
890 let config = TuiConfig::default();
891 assert_eq!(config.tick_rate_ms, 250);
892 assert_eq!(config.target_fps, 60);
893 assert!(!config.enable_mouse);
894 assert!(!config.skip_verification);
895 assert!(config.color_mode.is_none());
896 }
897
898 #[test]
899 fn test_config_high_performance() {
900 let config = TuiConfig::high_performance();
901 assert_eq!(config.tick_rate_ms, 16);
902 assert_eq!(config.target_fps, 60);
903 }
904
905 #[test]
906 fn test_config_power_saving() {
907 let config = TuiConfig::power_saving();
908 assert_eq!(config.tick_rate_ms, 100);
909 assert_eq!(config.target_fps, 30);
910 }
911
912 #[test]
913 fn test_tui_app_with_config() {
914 let widget = TestWidget::new();
915 let mut app = TuiApp::new(widget).unwrap();
916
917 let config = TuiConfig {
918 tick_rate_ms: 50,
919 enable_mouse: true,
920 color_mode: Some(ColorMode::Color256),
921 skip_verification: false,
922 target_fps: 30,
923 };
924
925 app = app.with_config(config);
926 assert!(app.metrics().frame_count == 0);
927 }
928
929 #[test]
930 fn test_tui_app_with_input_handler() {
931 let widget = TestWidget::new();
932 let mut app = TuiApp::new(widget).unwrap();
933
934 let mut handler = InputHandler::new();
935 handler.add_binding(crate::input::KeyBinding::simple(
936 crossterm::event::KeyCode::Char('q'),
937 "quit",
938 ));
939
940 app = app.with_input_handler(handler);
941 assert!(app.root().assertions().len() == 1);
942 }
943
944 #[test]
945 fn test_tui_app_root_accessors() {
946 let widget = TestWidget::new();
947 let mut app = TuiApp::new(widget).unwrap();
948
949 assert_eq!(app.root().brick_name(), "test_widget");
950 assert_eq!(app.root_mut().brick_name(), "test_widget");
951 }
952
953 #[test]
954 fn test_tui_app_metrics() {
955 let widget = TestWidget::new();
956 let app = TuiApp::new(widget).unwrap();
957
958 let metrics = app.metrics();
959 assert_eq!(metrics.frame_count, 0);
960 assert_eq!(metrics.total_time, Duration::ZERO);
961 }
962
963 #[test]
964 fn test_tui_app_quit() {
965 let widget = TestWidget::new();
966 let mut app = TuiApp::new(widget).unwrap();
967
968 assert!(!app.should_quit);
969 app.quit();
970 assert!(app.should_quit);
971 }
972
973 #[test]
974 fn test_frame_metrics_default() {
975 let metrics = FrameMetrics::default();
976 assert_eq!(metrics.frame_count, 0);
977 assert_eq!(metrics.verify_time, Duration::ZERO);
978 assert_eq!(metrics.measure_time, Duration::ZERO);
979 assert_eq!(metrics.layout_time, Duration::ZERO);
980 assert_eq!(metrics.paint_time, Duration::ZERO);
981 assert_eq!(metrics.total_time, Duration::ZERO);
982 }
983
984 #[test]
985 fn test_config_with_color_mode_override() {
986 let widget = TestWidget::new();
987 let app = TuiApp::new(widget).unwrap();
988
989 let config = TuiConfig {
990 color_mode: Some(ColorMode::Mono),
991 ..Default::default()
992 };
993
994 let app = app.with_config(config);
995 assert_eq!(app.color_mode, ColorMode::Mono);
996 }
997
998 #[test]
999 fn test_config_without_color_mode() {
1000 let widget = TestWidget::new();
1001 let app = TuiApp::new(widget).unwrap();
1002 let original_mode = app.color_mode;
1003
1004 let config = TuiConfig {
1005 color_mode: None,
1006 ..Default::default()
1007 };
1008
1009 let app = app.with_config(config);
1010 assert_eq!(app.color_mode, original_mode);
1011 }
1012
1013 #[test]
1014 fn test_render_frame() {
1015 let widget = TestWidget::new();
1016 let mut app = TuiApp::new(widget).unwrap();
1017 let mut buffer = CellBuffer::new(80, 24);
1018
1019 app.render_frame(&mut buffer);
1021
1022 assert!(
1023 app.metrics.measure_time > Duration::ZERO || app.metrics.measure_time == Duration::ZERO
1024 );
1025 assert!(app.metrics.layout_time >= Duration::ZERO);
1026 assert!(app.metrics.paint_time >= Duration::ZERO);
1027 }
1028
1029 #[test]
1030 fn test_render_frame_updates_metrics() {
1031 let widget = TestWidget::new();
1032 let mut app = TuiApp::new(widget).unwrap();
1033 let mut buffer = CellBuffer::new(40, 10);
1034
1035 for _ in 0..3 {
1037 app.render_frame(&mut buffer);
1038 }
1039
1040 let metrics = app.metrics();
1042 assert_eq!(metrics.frame_count, 0); }
1044
1045 #[test]
1046 fn test_render_frame_with_different_buffer_sizes() {
1047 let widget = TestWidget::new();
1048 let mut app = TuiApp::new(widget).unwrap();
1049
1050 let mut small_buffer = CellBuffer::new(10, 5);
1052 app.render_frame(&mut small_buffer);
1053
1054 let mut large_buffer = CellBuffer::new(200, 50);
1056 app.render_frame(&mut large_buffer);
1057
1058 }
1060
1061 #[test]
1062 fn test_frame_metrics_clone() {
1063 let metrics = FrameMetrics {
1064 verify_time: Duration::from_millis(1),
1065 measure_time: Duration::from_millis(2),
1066 layout_time: Duration::from_millis(3),
1067 paint_time: Duration::from_millis(4),
1068 total_time: Duration::from_millis(10),
1069 frame_count: 100,
1070 };
1071
1072 let cloned = metrics.clone();
1073 assert_eq!(cloned.frame_count, 100);
1074 assert_eq!(cloned.verify_time, Duration::from_millis(1));
1075 }
1076
1077 #[test]
1078 fn test_frame_metrics_debug() {
1079 let metrics = FrameMetrics::default();
1080 let debug_str = format!("{:?}", metrics);
1081 assert!(debug_str.contains("FrameMetrics"));
1082 assert!(debug_str.contains("frame_count"));
1083 }
1084
1085 #[test]
1086 fn test_tui_config_clone() {
1087 let config = TuiConfig::high_performance();
1088 let cloned = config.clone();
1089 assert_eq!(cloned.tick_rate_ms, 16);
1090 assert_eq!(cloned.target_fps, 60);
1091 }
1092
1093 #[test]
1094 fn test_tui_config_debug() {
1095 let config = TuiConfig::default();
1096 let debug_str = format!("{:?}", config);
1097 assert!(debug_str.contains("TuiConfig"));
1098 assert!(debug_str.contains("tick_rate_ms"));
1099 }
1100
1101 struct FailingWidget;
1104
1105 impl Brick for FailingWidget {
1106 fn brick_name(&self) -> &'static str {
1107 "failing_widget"
1108 }
1109
1110 fn assertions(&self) -> &[BrickAssertion] {
1111 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
1112 ASSERTIONS
1113 }
1114
1115 fn budget(&self) -> BrickBudget {
1116 BrickBudget::default()
1117 }
1118
1119 fn verify(&self) -> BrickVerification {
1120 BrickVerification {
1122 passed: vec![],
1123 failed: vec![(
1124 BrickAssertion::max_latency_ms(16),
1125 "Intentional failure".to_string(),
1126 )],
1127 verification_time: Duration::from_micros(10),
1128 }
1129 }
1130
1131 fn to_html(&self) -> String {
1132 String::new()
1133 }
1134
1135 fn to_css(&self) -> String {
1136 String::new()
1137 }
1138 }
1139
1140 impl Widget for FailingWidget {
1141 fn type_id(&self) -> TypeId {
1142 TypeId::of::<Self>()
1143 }
1144
1145 fn measure(&self, constraints: Constraints) -> Size {
1146 constraints.constrain(Size::new(10.0, 5.0))
1147 }
1148
1149 fn layout(&mut self, bounds: Rect) -> LayoutResult {
1150 LayoutResult {
1151 size: Size::new(bounds.width, bounds.height),
1152 }
1153 }
1154
1155 fn paint(&self, _canvas: &mut dyn Canvas) {}
1156
1157 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
1158 None
1159 }
1160
1161 fn children(&self) -> &[Box<dyn Widget>] {
1162 &[]
1163 }
1164
1165 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
1166 &mut []
1167 }
1168 }
1169
1170 #[test]
1171 fn test_tui_app_with_failing_widget() {
1172 let widget = FailingWidget;
1173 let app = TuiApp::new(widget);
1174 assert!(app.is_ok());
1176 }
1177
1178 #[test]
1179 fn test_tui_config_all_fields() {
1180 let config = TuiConfig {
1181 tick_rate_ms: 100,
1182 enable_mouse: true,
1183 color_mode: Some(ColorMode::Color16),
1184 skip_verification: true,
1185 target_fps: 30,
1186 };
1187
1188 assert_eq!(config.tick_rate_ms, 100);
1189 assert!(config.enable_mouse);
1190 assert_eq!(config.color_mode, Some(ColorMode::Color16));
1191 assert!(config.skip_verification);
1192 assert_eq!(config.target_fps, 30);
1193 }
1194
1195 #[test]
1196 fn test_frame_metrics_all_fields() {
1197 let metrics = FrameMetrics {
1198 verify_time: Duration::from_millis(1),
1199 measure_time: Duration::from_millis(2),
1200 layout_time: Duration::from_millis(3),
1201 paint_time: Duration::from_millis(4),
1202 total_time: Duration::from_millis(10),
1203 frame_count: 42,
1204 };
1205
1206 assert_eq!(metrics.verify_time, Duration::from_millis(1));
1207 assert_eq!(metrics.measure_time, Duration::from_millis(2));
1208 assert_eq!(metrics.layout_time, Duration::from_millis(3));
1209 assert_eq!(metrics.paint_time, Duration::from_millis(4));
1210 assert_eq!(metrics.total_time, Duration::from_millis(10));
1211 assert_eq!(metrics.frame_count, 42);
1212 }
1213
1214 #[test]
1215 fn test_tui_app_skip_verification_config() {
1216 let widget = TestWidget::new();
1217 let app = TuiApp::new(widget).unwrap();
1218
1219 let config = TuiConfig {
1220 skip_verification: true,
1221 ..Default::default()
1222 };
1223
1224 let app = app.with_config(config);
1225 assert!(app.config.skip_verification);
1226 }
1227
1228 #[test]
1229 fn test_tui_app_enable_mouse_config() {
1230 let widget = TestWidget::new();
1231 let app = TuiApp::new(widget).unwrap();
1232
1233 let config = TuiConfig {
1234 enable_mouse: true,
1235 ..Default::default()
1236 };
1237
1238 let app = app.with_config(config);
1239 assert!(app.config.enable_mouse);
1240 }
1241
1242 #[test]
1243 fn test_render_frame_zero_size_buffer() {
1244 let widget = TestWidget::new();
1245 let mut app = TuiApp::new(widget).unwrap();
1246
1247 let mut buffer = CellBuffer::new(1, 1);
1249 app.render_frame(&mut buffer);
1250 }
1252
1253 #[test]
1254 fn test_render_frame_metrics_populated() {
1255 let widget = TestWidget::new();
1256 let mut app = TuiApp::new(widget).unwrap();
1257 let mut buffer = CellBuffer::new(80, 24);
1258
1259 app.render_frame(&mut buffer);
1260
1261 assert!(app.metrics.measure_time >= Duration::ZERO);
1263 assert!(app.metrics.layout_time >= Duration::ZERO);
1264 assert!(app.metrics.paint_time >= Duration::ZERO);
1265 }
1266
1267 #[test]
1268 fn test_tui_config_color_modes() {
1269 for mode in [
1271 ColorMode::TrueColor,
1272 ColorMode::Color256,
1273 ColorMode::Color16,
1274 ColorMode::Mono,
1275 ] {
1276 let widget = TestWidget::new();
1277 let app = TuiApp::new(widget).unwrap();
1278
1279 let config = TuiConfig {
1280 color_mode: Some(mode),
1281 ..Default::default()
1282 };
1283
1284 let app = app.with_config(config);
1285 assert_eq!(app.color_mode, mode);
1286 }
1287 }
1288
1289 #[test]
1290 fn test_test_widget_brick_methods() {
1291 let widget = TestWidget::new();
1292
1293 assert_eq!(widget.brick_name(), "test_widget");
1294 assert!(!widget.assertions().is_empty());
1295 assert!(widget.verify().is_valid());
1296 assert!(widget.to_html().is_empty());
1297 assert!(widget.to_css().is_empty());
1298 }
1299
1300 #[test]
1301 fn test_test_widget_widget_methods() {
1302 let mut widget = TestWidget::new();
1303
1304 let size = widget.measure(Constraints::loose(Size::new(100.0, 100.0)));
1306 assert!(size.width > 0.0);
1307 assert!(size.height > 0.0);
1308
1309 let bounds = Rect::new(0.0, 0.0, 50.0, 25.0);
1311 let result = widget.layout(bounds);
1312 assert_eq!(result.size.width, 50.0);
1313 assert_eq!(result.size.height, 25.0);
1314
1315 let event = Event::KeyDown {
1317 key: presentar_core::Key::Enter,
1318 };
1319 assert!(widget.event(&event).is_none());
1320
1321 assert!(widget.children().is_empty());
1323 assert!(widget.children_mut().is_empty());
1324 }
1325
1326 #[test]
1327 fn test_tui_app_multiple_render_frames() {
1328 let widget = TestWidget::new();
1329 let mut app = TuiApp::new(widget).unwrap();
1330 let mut buffer = CellBuffer::new(80, 24);
1331
1332 for _ in 0..10 {
1334 app.render_frame(&mut buffer);
1335 }
1336
1337 }
1339
1340 use std::cell::RefCell;
1343 use std::collections::VecDeque;
1344
1345 struct MockTerminal {
1346 size: (u16, u16),
1347 events: RefCell<VecDeque<CrosstermEvent>>,
1348 poll_results: RefCell<VecDeque<bool>>,
1349 entered: RefCell<bool>,
1350 left: RefCell<bool>,
1351 mouse_enabled: RefCell<bool>,
1352 flush_count: RefCell<u32>,
1353 }
1354
1355 impl MockTerminal {
1356 fn new(width: u16, height: u16) -> Self {
1357 Self {
1358 size: (width, height),
1359 events: RefCell::new(VecDeque::new()),
1360 poll_results: RefCell::new(VecDeque::new()),
1361 entered: RefCell::new(false),
1362 left: RefCell::new(false),
1363 mouse_enabled: RefCell::new(false),
1364 flush_count: RefCell::new(0),
1365 }
1366 }
1367
1368 fn with_events(mut self, events: Vec<CrosstermEvent>) -> Self {
1369 self.events = RefCell::new(events.into());
1370 self
1371 }
1372
1373 fn with_polls(mut self, polls: Vec<bool>) -> Self {
1374 self.poll_results = RefCell::new(polls.into());
1375 self
1376 }
1377 }
1378
1379 impl Terminal for MockTerminal {
1380 fn enter(&mut self) -> Result<(), TuiError> {
1381 *self.entered.borrow_mut() = true;
1382 Ok(())
1383 }
1384
1385 fn leave(&mut self) -> Result<(), TuiError> {
1386 *self.left.borrow_mut() = true;
1387 Ok(())
1388 }
1389
1390 fn size(&self) -> Result<(u16, u16), TuiError> {
1391 Ok(self.size)
1392 }
1393
1394 fn poll(&self, _timeout: Duration) -> Result<bool, TuiError> {
1395 Ok(self.poll_results.borrow_mut().pop_front().unwrap_or(false))
1396 }
1397
1398 fn read_event(&self) -> Result<CrosstermEvent, TuiError> {
1399 self.events
1400 .borrow_mut()
1401 .pop_front()
1402 .ok_or_else(|| TuiError::Io(io::Error::new(io::ErrorKind::Other, "no event")))
1403 }
1404
1405 fn flush(
1406 &mut self,
1407 _buffer: &mut CellBuffer,
1408 _renderer: &mut DiffRenderer,
1409 ) -> Result<(), TuiError> {
1410 *self.flush_count.borrow_mut() += 1;
1411 Ok(())
1412 }
1413
1414 fn enable_mouse(&mut self) -> Result<(), TuiError> {
1415 *self.mouse_enabled.borrow_mut() = true;
1416 Ok(())
1417 }
1418
1419 fn disable_mouse(&mut self) -> Result<(), TuiError> {
1420 *self.mouse_enabled.borrow_mut() = false;
1421 Ok(())
1422 }
1423 }
1424
1425 #[test]
1426 fn test_run_with_terminal_quit_on_q() {
1427 let widget = TestWidget::new();
1428 let mut app = TuiApp::new(widget).unwrap();
1429
1430 let terminal = MockTerminal::new(80, 24)
1431 .with_polls(vec![true])
1432 .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1433 KeyCode::Char('q'),
1434 crossterm::event::KeyModifiers::NONE,
1435 ))]);
1436
1437 let result = app.run_with_terminal(terminal);
1438 assert!(result.is_ok());
1439 assert!(app.should_quit);
1440 }
1441
1442 #[test]
1443 fn test_run_with_terminal_ctrl_c() {
1444 let widget = TestWidget::new();
1445 let mut app = TuiApp::new(widget).unwrap();
1446
1447 let terminal = MockTerminal::new(80, 24)
1448 .with_polls(vec![true])
1449 .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1450 KeyCode::Char('c'),
1451 crossterm::event::KeyModifiers::CONTROL,
1452 ))]);
1453
1454 let result = app.run_with_terminal(terminal);
1455 assert!(result.is_ok());
1456 assert!(app.should_quit);
1457 }
1458
1459 #[test]
1460 fn test_run_with_terminal_mouse_enabled() {
1461 let widget = TestWidget::new();
1462 let mut app = TuiApp::new(widget).unwrap();
1463 app.config.enable_mouse = true;
1464
1465 let terminal = MockTerminal::new(80, 24)
1466 .with_polls(vec![true])
1467 .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1468 KeyCode::Char('q'),
1469 crossterm::event::KeyModifiers::NONE,
1470 ))]);
1471
1472 let result = app.run_with_terminal(terminal);
1473 assert!(result.is_ok());
1474 }
1475
1476 #[test]
1477 fn test_run_with_terminal_skip_verification() {
1478 let widget = FailingWidget;
1479 let mut app = TuiApp::new(widget).unwrap();
1480 app.config.skip_verification = true;
1481
1482 let terminal = MockTerminal::new(80, 24)
1483 .with_polls(vec![true])
1484 .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1485 KeyCode::Char('q'),
1486 crossterm::event::KeyModifiers::NONE,
1487 ))]);
1488
1489 let result = app.run_with_terminal(terminal);
1491 assert!(result.is_ok());
1492 }
1493
1494 #[test]
1495 fn test_run_with_terminal_verification_failure() {
1496 let widget = FailingWidget;
1497 let mut app = TuiApp::new(widget).unwrap();
1498
1499 let terminal = MockTerminal::new(80, 24).with_polls(vec![false]);
1500
1501 let result = app.run_with_terminal(terminal);
1503 assert!(result.is_err());
1504 assert!(matches!(result, Err(TuiError::VerificationFailed(_))));
1505 }
1506
1507 #[test]
1508 fn test_run_with_terminal_no_events() {
1509 let widget = TestWidget::new();
1510 let mut app = TuiApp::new(widget).unwrap();
1511 app.quit(); let terminal = MockTerminal::new(80, 24).with_polls(vec![false]);
1514
1515 let result = app.run_with_terminal(terminal);
1516 assert!(result.is_ok());
1517 }
1518
1519 #[test]
1520 fn test_run_with_terminal_other_key() {
1521 let widget = TestWidget::new();
1522 let mut app = TuiApp::new(widget).unwrap();
1523
1524 let terminal = MockTerminal::new(80, 24)
1525 .with_polls(vec![true, true])
1526 .with_events(vec![
1527 CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1528 KeyCode::Enter,
1529 crossterm::event::KeyModifiers::NONE,
1530 )),
1531 CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1532 KeyCode::Char('q'),
1533 crossterm::event::KeyModifiers::NONE,
1534 )),
1535 ]);
1536
1537 let result = app.run_with_terminal(terminal);
1538 assert!(result.is_ok());
1539 }
1540
1541 #[test]
1542 fn test_run_with_terminal_frame_count() {
1543 let widget = TestWidget::new();
1544 let mut app = TuiApp::new(widget).unwrap();
1545
1546 let terminal = MockTerminal::new(80, 24)
1547 .with_polls(vec![false, false, true])
1548 .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1549 KeyCode::Char('q'),
1550 crossterm::event::KeyModifiers::NONE,
1551 ))]);
1552
1553 let result = app.run_with_terminal(terminal);
1554 assert!(result.is_ok());
1555 assert!(app.metrics.frame_count >= 1);
1556 }
1557
1558 #[test]
1559 fn test_crossterm_backend_new() {
1560 let backend = CrosstermBackend::new();
1561 let _ = backend;
1563 }
1564
1565 #[test]
1566 fn test_crossterm_backend_default() {
1567 let backend = CrosstermBackend::default();
1568 let _ = backend;
1569 }
1570
1571 struct MockBackend {
1573 size: (u16, u16),
1574 events: RefCell<VecDeque<CrosstermEvent>>,
1575 poll_results: RefCell<VecDeque<bool>>,
1576 raw_mode: RefCell<bool>,
1577 alternate_screen: RefCell<bool>,
1578 cursor_hidden: RefCell<bool>,
1579 mouse_captured: RefCell<bool>,
1580 }
1581
1582 impl MockBackend {
1583 fn new(width: u16, height: u16) -> Self {
1584 Self {
1585 size: (width, height),
1586 events: RefCell::new(VecDeque::new()),
1587 poll_results: RefCell::new(VecDeque::new()),
1588 raw_mode: RefCell::new(false),
1589 alternate_screen: RefCell::new(false),
1590 cursor_hidden: RefCell::new(false),
1591 mouse_captured: RefCell::new(false),
1592 }
1593 }
1594
1595 fn with_events(self, events: Vec<CrosstermEvent>) -> Self {
1596 *self.events.borrow_mut() = events.into();
1597 self
1598 }
1599
1600 fn with_polls(self, polls: Vec<bool>) -> Self {
1601 *self.poll_results.borrow_mut() = polls.into();
1602 self
1603 }
1604 }
1605
1606 impl TerminalBackend for MockBackend {
1607 fn enable_raw_mode(&mut self) -> Result<(), TuiError> {
1608 *self.raw_mode.borrow_mut() = true;
1609 Ok(())
1610 }
1611 fn disable_raw_mode(&mut self) -> Result<(), TuiError> {
1612 *self.raw_mode.borrow_mut() = false;
1613 Ok(())
1614 }
1615 fn enter_alternate_screen(&mut self) -> Result<(), TuiError> {
1616 *self.alternate_screen.borrow_mut() = true;
1617 Ok(())
1618 }
1619 fn leave_alternate_screen(&mut self) -> Result<(), TuiError> {
1620 *self.alternate_screen.borrow_mut() = false;
1621 Ok(())
1622 }
1623 fn hide_cursor(&mut self) -> Result<(), TuiError> {
1624 *self.cursor_hidden.borrow_mut() = true;
1625 Ok(())
1626 }
1627 fn show_cursor(&mut self) -> Result<(), TuiError> {
1628 *self.cursor_hidden.borrow_mut() = false;
1629 Ok(())
1630 }
1631 fn size(&self) -> Result<(u16, u16), TuiError> {
1632 Ok(self.size)
1633 }
1634 fn poll(&self, _timeout: Duration) -> Result<bool, TuiError> {
1635 Ok(self.poll_results.borrow_mut().pop_front().unwrap_or(false))
1636 }
1637 fn read_event(&self) -> Result<CrosstermEvent, TuiError> {
1638 self.events
1639 .borrow_mut()
1640 .pop_front()
1641 .ok_or_else(|| TuiError::Io(io::Error::new(io::ErrorKind::Other, "no event")))
1642 }
1643 fn write_flush(
1644 &mut self,
1645 _buffer: &mut CellBuffer,
1646 _renderer: &mut DiffRenderer,
1647 ) -> Result<(), TuiError> {
1648 Ok(())
1649 }
1650 fn enable_mouse_capture(&mut self) -> Result<(), TuiError> {
1651 *self.mouse_captured.borrow_mut() = true;
1652 Ok(())
1653 }
1654 fn disable_mouse_capture(&mut self) -> Result<(), TuiError> {
1655 *self.mouse_captured.borrow_mut() = false;
1656 Ok(())
1657 }
1658 }
1659
1660 #[test]
1661 fn test_generic_terminal_enter_leave() {
1662 let backend = MockBackend::new(80, 24);
1663 let mut terminal = GenericTerminal::new(backend);
1664
1665 terminal.enter().unwrap();
1666 assert!(*terminal.backend.raw_mode.borrow());
1667 assert!(*terminal.backend.alternate_screen.borrow());
1668 assert!(*terminal.backend.cursor_hidden.borrow());
1669
1670 terminal.leave().unwrap();
1671 assert!(!*terminal.backend.raw_mode.borrow());
1672 assert!(!*terminal.backend.alternate_screen.borrow());
1673 assert!(!*terminal.backend.cursor_hidden.borrow());
1674 }
1675
1676 #[test]
1677 fn test_generic_terminal_size() {
1678 let backend = MockBackend::new(100, 50);
1679 let terminal = GenericTerminal::new(backend);
1680 let (w, h) = terminal.size().unwrap();
1681 assert_eq!(w, 100);
1682 assert_eq!(h, 50);
1683 }
1684
1685 #[test]
1686 fn test_generic_terminal_poll_read() {
1687 let backend = MockBackend::new(80, 24)
1688 .with_polls(vec![true, false])
1689 .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1690 KeyCode::Enter,
1691 crossterm::event::KeyModifiers::NONE,
1692 ))]);
1693 let terminal = GenericTerminal::new(backend);
1694
1695 assert!(terminal.poll(Duration::from_millis(10)).unwrap());
1696 let event = terminal.read_event().unwrap();
1697 assert!(matches!(event, CrosstermEvent::Key(_)));
1698
1699 assert!(!terminal.poll(Duration::from_millis(10)).unwrap());
1700 }
1701
1702 #[test]
1703 fn test_generic_terminal_mouse() {
1704 let backend = MockBackend::new(80, 24);
1705 let mut terminal = GenericTerminal::new(backend);
1706
1707 assert!(!*terminal.backend.mouse_captured.borrow());
1708 terminal.enable_mouse().unwrap();
1709 assert!(*terminal.backend.mouse_captured.borrow());
1710 terminal.disable_mouse().unwrap();
1711 assert!(!*terminal.backend.mouse_captured.borrow());
1712 }
1713
1714 #[test]
1715 fn test_generic_terminal_flush() {
1716 let backend = MockBackend::new(80, 24);
1717 let mut terminal = GenericTerminal::new(backend);
1718 let mut buffer = CellBuffer::new(80, 24);
1719 let mut renderer = DiffRenderer::new();
1720
1721 terminal.flush(&mut buffer, &mut renderer).unwrap();
1722 }
1723
1724 #[test]
1725 fn test_run_with_generic_terminal() {
1726 let widget = TestWidget::new();
1727 let mut app = TuiApp::new(widget).unwrap();
1728
1729 let backend = MockBackend::new(80, 24)
1730 .with_polls(vec![true])
1731 .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1732 KeyCode::Char('q'),
1733 crossterm::event::KeyModifiers::NONE,
1734 ))]);
1735 let terminal = GenericTerminal::new(backend);
1736
1737 let result = app.run_with_terminal(terminal);
1738 assert!(result.is_ok());
1739 assert!(app.should_quit);
1740 }
1741
1742 #[test]
1743 fn test_mock_terminal_enter_leave() {
1744 let mut terminal = MockTerminal::new(80, 24);
1745 assert!(!*terminal.entered.borrow());
1746 terminal.enter().unwrap();
1747 assert!(*terminal.entered.borrow());
1748
1749 assert!(!*terminal.left.borrow());
1750 terminal.leave().unwrap();
1751 assert!(*terminal.left.borrow());
1752 }
1753
1754 #[test]
1755 fn test_mock_terminal_mouse() {
1756 let mut terminal = MockTerminal::new(80, 24);
1757 assert!(!*terminal.mouse_enabled.borrow());
1758 terminal.enable_mouse().unwrap();
1759 assert!(*terminal.mouse_enabled.borrow());
1760 terminal.disable_mouse().unwrap();
1761 assert!(!*terminal.mouse_enabled.borrow());
1762 }
1763
1764 #[test]
1765 fn test_mock_terminal_size() {
1766 let terminal = MockTerminal::new(100, 50);
1767 let (w, h) = terminal.size().unwrap();
1768 assert_eq!(w, 100);
1769 assert_eq!(h, 50);
1770 }
1771
1772 #[test]
1773 fn test_run_with_terminal_resize() {
1774 let widget = TestWidget::new();
1775 let mut app = TuiApp::new(widget).unwrap();
1776
1777 let terminal = MockTerminal::new(40, 12)
1779 .with_polls(vec![true])
1780 .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1781 KeyCode::Char('q'),
1782 crossterm::event::KeyModifiers::NONE,
1783 ))]);
1784
1785 let result = app.run_with_terminal(terminal);
1786 assert!(result.is_ok());
1787 }
1788
1789 #[test]
1790 fn test_run_with_terminal_mouse_event() {
1791 let widget = TestWidget::new();
1792 let mut app = TuiApp::new(widget).unwrap();
1793
1794 let terminal = MockTerminal::new(80, 24)
1795 .with_polls(vec![true, true])
1796 .with_events(vec![
1797 CrosstermEvent::Mouse(crossterm::event::MouseEvent {
1798 kind: crossterm::event::MouseEventKind::Down(
1799 crossterm::event::MouseButton::Left,
1800 ),
1801 column: 10,
1802 row: 5,
1803 modifiers: crossterm::event::KeyModifiers::NONE,
1804 }),
1805 CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1806 KeyCode::Char('q'),
1807 crossterm::event::KeyModifiers::NONE,
1808 )),
1809 ]);
1810
1811 let result = app.run_with_terminal(terminal);
1812 assert!(result.is_ok());
1813 }
1814
1815 #[test]
1816 fn test_run_with_terminal_non_key_event_then_quit() {
1817 let widget = TestWidget::new();
1818 let mut app = TuiApp::new(widget).unwrap();
1819
1820 let terminal = MockTerminal::new(80, 24)
1821 .with_polls(vec![true, true])
1822 .with_events(vec![
1823 CrosstermEvent::Resize(100, 50),
1824 CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1825 KeyCode::Char('q'),
1826 crossterm::event::KeyModifiers::NONE,
1827 )),
1828 ]);
1829
1830 let result = app.run_with_terminal(terminal);
1831 assert!(result.is_ok());
1832 }
1833
1834 #[test]
1835 fn test_app_runner_metrics_update() {
1836 let widget = TestWidget::new();
1837 let mut app = TuiApp::new(widget).unwrap();
1838
1839 let terminal = MockTerminal::new(80, 24)
1840 .with_polls(vec![false, true])
1841 .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1842 KeyCode::Char('q'),
1843 crossterm::event::KeyModifiers::NONE,
1844 ))]);
1845
1846 app.run_with_terminal(terminal).unwrap();
1847
1848 assert!(app.metrics.frame_count >= 1);
1850 }
1851
1852 #[test]
1857 fn test_testable_backend_new() {
1858 let buf: Vec<u8> = Vec::new();
1859 let backend = TestableBackend::new(buf, 80, 24);
1860 assert_eq!(backend.size, (80, 24));
1861 assert!(!backend.raw_mode);
1862 assert!(!backend.alternate_screen);
1863 assert!(!backend.cursor_hidden);
1864 assert!(!backend.mouse_captured);
1865 }
1866
1867 #[test]
1868 fn test_testable_backend_with_events() {
1869 let buf: Vec<u8> = Vec::new();
1870 let backend = TestableBackend::new(buf, 80, 24).with_events(vec![CrosstermEvent::Key(
1871 crossterm::event::KeyEvent::new(
1872 KeyCode::Char('a'),
1873 crossterm::event::KeyModifiers::NONE,
1874 ),
1875 )]);
1876 assert_eq!(backend.events.borrow().len(), 1);
1877 }
1878
1879 #[test]
1880 fn test_testable_backend_with_polls() {
1881 let buf: Vec<u8> = Vec::new();
1882 let backend = TestableBackend::new(buf, 80, 24).with_polls(vec![true, false, true]);
1883 assert_eq!(backend.poll_results.borrow().len(), 3);
1884 }
1885
1886 #[test]
1887 fn test_testable_backend_enable_raw_mode() {
1888 let buf: Vec<u8> = Vec::new();
1889 let mut backend = TestableBackend::new(buf, 80, 24);
1890 assert!(!backend.is_raw_mode());
1891 backend.enable_raw_mode().unwrap();
1892 assert!(backend.is_raw_mode());
1893 }
1894
1895 #[test]
1896 fn test_testable_backend_disable_raw_mode() {
1897 let buf: Vec<u8> = Vec::new();
1898 let mut backend = TestableBackend::new(buf, 80, 24);
1899 backend.enable_raw_mode().unwrap();
1900 assert!(backend.is_raw_mode());
1901 backend.disable_raw_mode().unwrap();
1902 assert!(!backend.is_raw_mode());
1903 }
1904
1905 #[test]
1906 fn test_testable_backend_enter_alternate_screen() {
1907 let buf: Vec<u8> = Vec::new();
1908 let mut backend = TestableBackend::new(buf, 80, 24);
1909 assert!(!backend.is_alternate_screen());
1910 backend.enter_alternate_screen().unwrap();
1911 assert!(backend.is_alternate_screen());
1912 let output = backend.into_writer();
1914 assert!(!output.is_empty());
1915 assert!(output.starts_with(b"\x1b["));
1917 }
1918
1919 #[test]
1920 fn test_testable_backend_leave_alternate_screen() {
1921 let buf: Vec<u8> = Vec::new();
1922 let mut backend = TestableBackend::new(buf, 80, 24);
1923 backend.enter_alternate_screen().unwrap();
1924 backend.leave_alternate_screen().unwrap();
1925 assert!(!backend.is_alternate_screen());
1926 }
1927
1928 #[test]
1929 fn test_testable_backend_hide_cursor() {
1930 let buf: Vec<u8> = Vec::new();
1931 let mut backend = TestableBackend::new(buf, 80, 24);
1932 assert!(!backend.is_cursor_hidden());
1933 backend.hide_cursor().unwrap();
1934 assert!(backend.is_cursor_hidden());
1935 let output = backend.into_writer();
1937 assert!(!output.is_empty());
1938 }
1939
1940 #[test]
1941 fn test_testable_backend_show_cursor() {
1942 let buf: Vec<u8> = Vec::new();
1943 let mut backend = TestableBackend::new(buf, 80, 24);
1944 backend.hide_cursor().unwrap();
1945 backend.show_cursor().unwrap();
1946 assert!(!backend.is_cursor_hidden());
1947 }
1948
1949 #[test]
1950 fn test_testable_backend_size() {
1951 let buf: Vec<u8> = Vec::new();
1952 let backend = TestableBackend::new(buf, 120, 40);
1953 assert_eq!(backend.size().unwrap(), (120, 40));
1954 }
1955
1956 #[test]
1957 fn test_testable_backend_poll() {
1958 let buf: Vec<u8> = Vec::new();
1959 let backend = TestableBackend::new(buf, 80, 24).with_polls(vec![true, false]);
1960 assert!(backend.poll(Duration::from_millis(100)).unwrap());
1961 assert!(!backend.poll(Duration::from_millis(100)).unwrap());
1962 assert!(!backend.poll(Duration::from_millis(100)).unwrap());
1964 }
1965
1966 #[test]
1967 fn test_testable_backend_read_event() {
1968 let buf: Vec<u8> = Vec::new();
1969 let backend = TestableBackend::new(buf, 80, 24).with_events(vec![CrosstermEvent::Key(
1970 crossterm::event::KeyEvent::new(
1971 KeyCode::Char('x'),
1972 crossterm::event::KeyModifiers::NONE,
1973 ),
1974 )]);
1975 let event = backend.read_event().unwrap();
1976 assert!(matches!(event, CrosstermEvent::Key(_)));
1977 }
1978
1979 #[test]
1980 fn test_testable_backend_read_event_empty() {
1981 let buf: Vec<u8> = Vec::new();
1982 let backend = TestableBackend::new(buf, 80, 24);
1983 let result = backend.read_event();
1984 assert!(result.is_err());
1985 }
1986
1987 #[test]
1988 fn test_testable_backend_enable_mouse_capture() {
1989 let buf: Vec<u8> = Vec::new();
1990 let mut backend = TestableBackend::new(buf, 80, 24);
1991 assert!(!backend.is_mouse_captured());
1992 backend.enable_mouse_capture().unwrap();
1993 assert!(backend.is_mouse_captured());
1994 let output = backend.into_writer();
1996 assert!(!output.is_empty());
1997 }
1998
1999 #[test]
2000 fn test_testable_backend_disable_mouse_capture() {
2001 let buf: Vec<u8> = Vec::new();
2002 let mut backend = TestableBackend::new(buf, 80, 24);
2003 backend.enable_mouse_capture().unwrap();
2004 backend.disable_mouse_capture().unwrap();
2005 assert!(!backend.is_mouse_captured());
2006 }
2007
2008 #[test]
2009 fn test_testable_backend_write_flush() {
2010 use crate::direct::Cell;
2011
2012 let buf: Vec<u8> = Vec::new();
2013 let mut backend = TestableBackend::new(buf, 80, 24);
2014 let mut buffer = CellBuffer::new(80, 24);
2015 let mut renderer = DiffRenderer::new();
2016
2017 let mut cell_a = Cell::default();
2019 cell_a.update(
2020 "A",
2021 presentar_core::Color::WHITE,
2022 presentar_core::Color::BLACK,
2023 crate::direct::Modifiers::empty(),
2024 );
2025 buffer.set(0, 0, cell_a);
2026
2027 let mut cell_b = Cell::default();
2028 cell_b.update(
2029 "B",
2030 presentar_core::Color::WHITE,
2031 presentar_core::Color::BLACK,
2032 crate::direct::Modifiers::empty(),
2033 );
2034 buffer.set(1, 0, cell_b);
2035
2036 buffer.mark_all_dirty();
2037 backend.write_flush(&mut buffer, &mut renderer).unwrap();
2038
2039 let output = backend.into_writer();
2041 assert!(!output.is_empty());
2042 }
2043
2044 #[test]
2045 fn test_testable_backend_full_lifecycle() {
2046 let buf: Vec<u8> = Vec::new();
2047 let mut backend = TestableBackend::new(buf, 80, 24);
2048
2049 backend.enable_raw_mode().unwrap();
2051 backend.enter_alternate_screen().unwrap();
2052 backend.hide_cursor().unwrap();
2053
2054 assert!(backend.is_raw_mode());
2055 assert!(backend.is_alternate_screen());
2056 assert!(backend.is_cursor_hidden());
2057
2058 backend.show_cursor().unwrap();
2060 backend.leave_alternate_screen().unwrap();
2061 backend.disable_raw_mode().unwrap();
2062
2063 assert!(!backend.is_raw_mode());
2064 assert!(!backend.is_alternate_screen());
2065 assert!(!backend.is_cursor_hidden());
2066 }
2067
2068 #[test]
2069 fn test_testable_backend_escape_sequences() {
2070 let buf: Vec<u8> = Vec::new();
2071 let mut backend = TestableBackend::new(buf, 80, 24);
2072
2073 backend.enter_alternate_screen().unwrap();
2074 backend.hide_cursor().unwrap();
2075 backend.enable_mouse_capture().unwrap();
2076
2077 let output = backend.into_writer();
2078 let output_str = String::from_utf8_lossy(&output);
2079
2080 assert!(
2082 output_str.contains("\x1b["),
2083 "Expected ANSI escape sequences"
2084 );
2085 }
2086
2087 #[test]
2088 fn test_generic_terminal_with_testable_backend() {
2089 let buf: Vec<u8> = Vec::new();
2090 let backend = TestableBackend::new(buf, 80, 24)
2091 .with_polls(vec![true])
2092 .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
2093 KeyCode::Char('q'),
2094 crossterm::event::KeyModifiers::NONE,
2095 ))]);
2096
2097 let mut terminal = GenericTerminal::new(backend);
2098
2099 terminal.enter().unwrap();
2100 assert_eq!(terminal.size().unwrap(), (80, 24));
2101
2102 assert!(terminal.poll(Duration::from_millis(10)).unwrap());
2104 let event = terminal.read_event().unwrap();
2105 assert!(matches!(event, CrosstermEvent::Key(_)));
2106
2107 terminal.leave().unwrap();
2108 }
2109
2110 #[test]
2111 fn test_testable_backend_with_tui_app() {
2112 let widget = TestWidget::new();
2113 let mut app = TuiApp::new(widget).unwrap();
2114
2115 let buf: Vec<u8> = Vec::new();
2116 let backend = TestableBackend::new(buf, 80, 24)
2117 .with_polls(vec![true])
2118 .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
2119 KeyCode::Char('q'),
2120 crossterm::event::KeyModifiers::NONE,
2121 ))]);
2122
2123 let terminal = GenericTerminal::new(backend);
2124 let result = app.run_with_terminal(terminal);
2125 assert!(result.is_ok());
2126 }
2127
2128 #[test]
2129 fn test_testable_backend_captures_render_output() {
2130 let widget = TestWidget::new();
2131 let _app = TuiApp::new(widget).unwrap();
2132
2133 let buf: Vec<u8> = Vec::new();
2134 let backend = TestableBackend::new(buf, 40, 10)
2135 .with_polls(vec![true])
2136 .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
2137 KeyCode::Char('q'),
2138 crossterm::event::KeyModifiers::NONE,
2139 ))]);
2140
2141 let mut terminal = GenericTerminal::new(backend);
2142 terminal.enter().unwrap();
2143
2144 let (width, height) = terminal.size().unwrap();
2146 assert_eq!((width, height), (40, 10));
2147
2148 terminal.leave().unwrap();
2149 }
2150}