Skip to main content

jugar_probar/tui/
tty.rs

1//! TTY mocking for terminal testing.
2//!
3//! This module provides mock TTY functionality that captures ANSI escape sequences
4//! and tracks terminal state for testing purposes.
5
6use std::collections::VecDeque;
7use std::io::{self, Write};
8use std::time::Duration;
9
10use crossterm::event::Event;
11
12/// Mock TTY backend that captures output and tracks terminal state.
13///
14/// This struct allows testing terminal applications without a real TTY by:
15/// - Capturing all output (including ANSI escape sequences)
16/// - Tracking terminal state (raw mode, alternate screen, etc.)
17/// - Providing mock events for input simulation
18#[derive(Debug)]
19pub struct MockTty {
20    output: Vec<u8>,
21    size: (u16, u16),
22    raw_mode: bool,
23    alternate_screen: bool,
24    cursor_visible: bool,
25    mouse_captured: bool,
26    events: VecDeque<Event>,
27    poll_results: VecDeque<bool>,
28}
29
30impl MockTty {
31    /// Create a new mock TTY with the given dimensions.
32    pub fn new(width: u16, height: u16) -> Self {
33        Self {
34            output: Vec::new(),
35            size: (width, height),
36            raw_mode: false,
37            alternate_screen: false,
38            cursor_visible: true,
39            mouse_captured: false,
40            events: VecDeque::new(),
41            poll_results: VecDeque::new(),
42        }
43    }
44
45    /// Queue events to be returned by `read_event()`.
46    pub fn with_events(mut self, events: Vec<Event>) -> Self {
47        self.events = events.into_iter().collect();
48        self
49    }
50
51    /// Queue poll results to be returned by `poll()`.
52    pub fn with_polls(mut self, polls: Vec<bool>) -> Self {
53        self.poll_results = polls.into_iter().collect();
54        self
55    }
56
57    /// Get the terminal size.
58    pub fn size(&self) -> (u16, u16) {
59        self.size
60    }
61
62    /// Set the terminal size (for resize simulation).
63    pub fn set_size(&mut self, width: u16, height: u16) {
64        self.size = (width, height);
65    }
66
67    /// Check if raw mode is enabled.
68    pub fn is_raw_mode(&self) -> bool {
69        self.raw_mode
70    }
71
72    /// Enable raw mode.
73    pub fn enable_raw_mode(&mut self) {
74        self.raw_mode = true;
75    }
76
77    /// Disable raw mode.
78    pub fn disable_raw_mode(&mut self) {
79        self.raw_mode = false;
80    }
81
82    /// Check if alternate screen is active.
83    pub fn is_alternate_screen(&self) -> bool {
84        self.alternate_screen
85    }
86
87    /// Enter alternate screen.
88    pub fn enter_alternate_screen(&mut self) {
89        self.alternate_screen = true;
90        // Write the escape sequence
91        let _ = self.output.write_all(b"\x1b[?1049h");
92    }
93
94    /// Leave alternate screen.
95    pub fn leave_alternate_screen(&mut self) {
96        self.alternate_screen = false;
97        let _ = self.output.write_all(b"\x1b[?1049l");
98    }
99
100    /// Check if cursor is visible.
101    pub fn is_cursor_visible(&self) -> bool {
102        self.cursor_visible
103    }
104
105    /// Hide cursor.
106    pub fn hide_cursor(&mut self) {
107        self.cursor_visible = false;
108        let _ = self.output.write_all(b"\x1b[?25l");
109    }
110
111    /// Show cursor.
112    pub fn show_cursor(&mut self) {
113        self.cursor_visible = true;
114        let _ = self.output.write_all(b"\x1b[?25h");
115    }
116
117    /// Check if mouse capture is enabled.
118    pub fn is_mouse_captured(&self) -> bool {
119        self.mouse_captured
120    }
121
122    /// Enable mouse capture.
123    pub fn enable_mouse_capture(&mut self) {
124        self.mouse_captured = true;
125        let _ = self
126            .output
127            .write_all(b"\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h");
128    }
129
130    /// Disable mouse capture.
131    pub fn disable_mouse_capture(&mut self) {
132        self.mouse_captured = false;
133        let _ = self
134            .output
135            .write_all(b"\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l");
136    }
137
138    /// Poll for events with timeout.
139    pub fn poll(&mut self, _timeout: Duration) -> io::Result<bool> {
140        Ok(self.poll_results.pop_front().unwrap_or(false))
141    }
142
143    /// Read the next event.
144    pub fn read_event(&mut self) -> io::Result<Event> {
145        self.events
146            .pop_front()
147            .ok_or_else(|| io::Error::new(io::ErrorKind::WouldBlock, "no events available"))
148    }
149
150    /// Get the captured output bytes.
151    pub fn output(&self) -> &[u8] {
152        &self.output
153    }
154
155    /// Get the captured output as a string (lossy UTF-8 conversion).
156    pub fn output_str(&self) -> String {
157        String::from_utf8_lossy(&self.output).into_owned()
158    }
159
160    /// Clear the captured output.
161    pub fn clear_output(&mut self) {
162        self.output.clear();
163    }
164
165    /// Check if the output contains a specific byte sequence.
166    /// Returns false for empty needle (consistent with windows(0) behavior).
167    pub fn output_contains(&self, needle: &[u8]) -> bool {
168        if needle.is_empty() {
169            return false;
170        }
171        self.output
172            .windows(needle.len())
173            .any(|window| window == needle)
174    }
175
176    /// Check if the output contains a specific string.
177    pub fn output_contains_str(&self, needle: &str) -> bool {
178        self.output_contains(needle.as_bytes())
179    }
180
181    /// Check if the output contains an ANSI escape sequence.
182    pub fn contains_escape(&self, seq: &str) -> bool {
183        let escape_seq = format!("\x1b[{}", seq);
184        self.output_contains_str(&escape_seq)
185    }
186
187    /// Parse output into ANSI commands.
188    pub fn parsed_commands(&self) -> Vec<AnsiCommand> {
189        parse_ansi_commands(&self.output)
190    }
191
192    /// Get the number of queued events.
193    pub fn queued_events(&self) -> usize {
194        self.events.len()
195    }
196
197    /// Add an event to the queue.
198    pub fn push_event(&mut self, event: Event) {
199        self.events.push_back(event);
200    }
201
202    /// Add a poll result to the queue.
203    pub fn push_poll(&mut self, result: bool) {
204        self.poll_results.push_back(result);
205    }
206}
207
208impl Write for MockTty {
209    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
210        self.output.extend_from_slice(buf);
211        Ok(buf.len())
212    }
213
214    fn flush(&mut self) -> io::Result<()> {
215        Ok(())
216    }
217}
218
219impl Default for MockTty {
220    fn default() -> Self {
221        Self::new(80, 24)
222    }
223}
224
225/// Parsed ANSI command for testing assertions.
226#[derive(Debug, Clone, PartialEq, Eq)]
227pub enum AnsiCommand {
228    /// Cursor movement: CUP (H), CUU (A), CUD (B), CUF (C), CUB (D)
229    CursorMove {
230        /// Row position (1-based)
231        row: u16,
232        /// Column position (1-based)
233        col: u16,
234    },
235    /// Clear screen (ED)
236    ClearScreen(ClearMode),
237    /// Clear line (EL)
238    ClearLine(ClearMode),
239    /// Set graphics rendition (SGR)
240    SetAttribute(Vec<u8>),
241    /// Enter alternate screen
242    EnterAlternateScreen,
243    /// Leave alternate screen
244    LeaveAlternateScreen,
245    /// Hide cursor
246    HideCursor,
247    /// Show cursor
248    ShowCursor,
249    /// Enable mouse capture
250    EnableMouse,
251    /// Disable mouse capture
252    DisableMouse,
253    /// Plain text (non-escape content)
254    Text(String),
255    /// Unknown or unparsed escape sequence
256    Unknown(Vec<u8>),
257}
258
259/// Clear mode for ED (erase display) and EL (erase line) commands.
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261pub enum ClearMode {
262    /// Clear from cursor to end
263    ToEnd,
264    /// Clear from beginning to cursor
265    ToBeginning,
266    /// Clear entire screen/line
267    All,
268}
269
270/// Parse ANSI escape sequences from raw output.
271fn parse_ansi_commands(output: &[u8]) -> Vec<AnsiCommand> {
272    let mut commands = Vec::new();
273    let mut i = 0;
274    let mut text_start = 0;
275
276    while i < output.len() {
277        if output[i] == 0x1b && i + 1 < output.len() && output[i + 1] == b'[' {
278            // Flush pending text
279            if text_start < i {
280                if let Ok(text) = std::str::from_utf8(&output[text_start..i]) {
281                    if !text.is_empty() {
282                        commands.push(AnsiCommand::Text(text.to_string()));
283                    }
284                }
285            }
286
287            // Parse CSI sequence
288            let seq_start = i;
289            i += 2; // Skip ESC [
290
291            // Collect parameter bytes (0x30-0x3F)
292            let params_start = i;
293            while i < output.len() && (0x30..=0x3F).contains(&output[i]) {
294                i += 1;
295            }
296            let params = &output[params_start..i];
297
298            // Collect intermediate bytes (0x20-0x2F)
299            while i < output.len() && (0x20..=0x2F).contains(&output[i]) {
300                i += 1;
301            }
302
303            // Get final byte (0x40-0x7E)
304            if i < output.len() && (0x40..=0x7E).contains(&output[i]) {
305                let final_byte = output[i];
306                i += 1;
307
308                let cmd = parse_csi_command(params, final_byte);
309                commands.push(cmd);
310            } else {
311                // Incomplete or invalid sequence
312                commands.push(AnsiCommand::Unknown(output[seq_start..i].to_vec()));
313            }
314
315            text_start = i;
316        } else {
317            i += 1;
318        }
319    }
320
321    // Flush remaining text
322    if text_start < output.len() {
323        if let Ok(text) = std::str::from_utf8(&output[text_start..]) {
324            if !text.is_empty() {
325                commands.push(AnsiCommand::Text(text.to_string()));
326            }
327        }
328    }
329
330    commands
331}
332
333/// Parse a CSI sequence into an AnsiCommand.
334fn parse_csi_command(params: &[u8], final_byte: u8) -> AnsiCommand {
335    let params_str = std::str::from_utf8(params).unwrap_or("");
336
337    match final_byte {
338        b'H' | b'f' => {
339            // CUP - Cursor Position
340            let parts: Vec<u16> = params_str
341                .split(';')
342                .filter_map(|s| s.parse().ok())
343                .collect();
344            let row = parts.first().copied().unwrap_or(1);
345            let col = parts.get(1).copied().unwrap_or(1);
346            AnsiCommand::CursorMove { row, col }
347        }
348        b'J' => {
349            // ED - Erase Display
350            let mode = match params_str {
351                "" | "0" => ClearMode::ToEnd,
352                "1" => ClearMode::ToBeginning,
353                "2" | "3" => ClearMode::All,
354                _ => ClearMode::ToEnd,
355            };
356            AnsiCommand::ClearScreen(mode)
357        }
358        b'K' => {
359            // EL - Erase Line
360            let mode = match params_str {
361                "" | "0" => ClearMode::ToEnd,
362                "1" => ClearMode::ToBeginning,
363                "2" => ClearMode::All,
364                _ => ClearMode::ToEnd,
365            };
366            AnsiCommand::ClearLine(mode)
367        }
368        b'm' => {
369            // SGR - Set Graphics Rendition
370            let attrs: Vec<u8> = params_str
371                .split(';')
372                .filter_map(|s| s.parse().ok())
373                .collect();
374            AnsiCommand::SetAttribute(attrs)
375        }
376        b'h' => {
377            // SM - Set Mode (private modes with ?)
378            if params_str == "?1049" {
379                AnsiCommand::EnterAlternateScreen
380            } else if params_str == "?25" {
381                AnsiCommand::ShowCursor
382            } else if params_str.starts_with("?1000") || params_str.starts_with("?1002") {
383                AnsiCommand::EnableMouse
384            } else {
385                AnsiCommand::Unknown(format!("\x1b[{}h", params_str).into_bytes())
386            }
387        }
388        b'l' => {
389            // RM - Reset Mode (private modes with ?)
390            if params_str == "?1049" {
391                AnsiCommand::LeaveAlternateScreen
392            } else if params_str == "?25" {
393                AnsiCommand::HideCursor
394            } else if params_str.starts_with("?1000") || params_str.starts_with("?1006") {
395                AnsiCommand::DisableMouse
396            } else {
397                AnsiCommand::Unknown(format!("\x1b[{}l", params_str).into_bytes())
398            }
399        }
400        _ => {
401            // Unknown command
402            AnsiCommand::Unknown(format!("\x1b[{}{}", params_str, final_byte as char).into_bytes())
403        }
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
411
412    #[test]
413    fn test_new() {
414        let tty = MockTty::new(120, 40);
415        assert_eq!(tty.size(), (120, 40));
416        assert!(!tty.is_raw_mode());
417        assert!(!tty.is_alternate_screen());
418        assert!(tty.is_cursor_visible());
419        assert!(!tty.is_mouse_captured());
420    }
421
422    #[test]
423    fn test_default() {
424        let tty = MockTty::default();
425        assert_eq!(tty.size(), (80, 24));
426    }
427
428    #[test]
429    fn test_raw_mode() {
430        let mut tty = MockTty::new(80, 24);
431        assert!(!tty.is_raw_mode());
432        tty.enable_raw_mode();
433        assert!(tty.is_raw_mode());
434        tty.disable_raw_mode();
435        assert!(!tty.is_raw_mode());
436    }
437
438    #[test]
439    fn test_alternate_screen() {
440        let mut tty = MockTty::new(80, 24);
441        assert!(!tty.is_alternate_screen());
442        tty.enter_alternate_screen();
443        assert!(tty.is_alternate_screen());
444        assert!(tty.output_contains_str("\x1b[?1049h"));
445        tty.leave_alternate_screen();
446        assert!(!tty.is_alternate_screen());
447        assert!(tty.output_contains_str("\x1b[?1049l"));
448    }
449
450    #[test]
451    fn test_cursor_visibility() {
452        let mut tty = MockTty::new(80, 24);
453        assert!(tty.is_cursor_visible());
454        tty.hide_cursor();
455        assert!(!tty.is_cursor_visible());
456        assert!(tty.output_contains_str("\x1b[?25l"));
457        tty.show_cursor();
458        assert!(tty.is_cursor_visible());
459        assert!(tty.output_contains_str("\x1b[?25h"));
460    }
461
462    #[test]
463    fn test_mouse_capture() {
464        let mut tty = MockTty::new(80, 24);
465        assert!(!tty.is_mouse_captured());
466        tty.enable_mouse_capture();
467        assert!(tty.is_mouse_captured());
468        tty.disable_mouse_capture();
469        assert!(!tty.is_mouse_captured());
470    }
471
472    #[test]
473    fn test_write() {
474        let mut tty = MockTty::new(80, 24);
475        tty.write_all(b"Hello, World!").unwrap();
476        assert_eq!(tty.output(), b"Hello, World!");
477        assert_eq!(tty.output_str(), "Hello, World!");
478    }
479
480    #[test]
481    fn test_output_contains() {
482        let mut tty = MockTty::new(80, 24);
483        tty.write_all(b"Hello, World!").unwrap();
484        assert!(tty.output_contains(b"World"));
485        assert!(tty.output_contains_str("Hello"));
486        assert!(!tty.output_contains_str("Goodbye"));
487    }
488
489    #[test]
490    fn test_clear_output() {
491        let mut tty = MockTty::new(80, 24);
492        tty.write_all(b"Hello").unwrap();
493        assert!(!tty.output().is_empty());
494        tty.clear_output();
495        assert!(tty.output().is_empty());
496    }
497
498    #[test]
499    fn test_events() {
500        let tty = MockTty::new(80, 24).with_events(vec![
501            Event::Key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)),
502            Event::Key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE)),
503        ]);
504        assert_eq!(tty.queued_events(), 2);
505    }
506
507    #[test]
508    fn test_read_event() {
509        let mut tty = MockTty::new(80, 24).with_events(vec![Event::Key(KeyEvent::new(
510            KeyCode::Char('x'),
511            KeyModifiers::NONE,
512        ))]);
513        let event = tty.read_event().unwrap();
514        assert!(matches!(event, Event::Key(_)));
515        assert!(tty.read_event().is_err()); // No more events
516    }
517
518    #[test]
519    fn test_poll() {
520        let mut tty = MockTty::new(80, 24).with_polls(vec![true, false, true]);
521        assert!(tty.poll(Duration::from_millis(100)).unwrap());
522        assert!(!tty.poll(Duration::from_millis(100)).unwrap());
523        assert!(tty.poll(Duration::from_millis(100)).unwrap());
524        assert!(!tty.poll(Duration::from_millis(100)).unwrap()); // Default false
525    }
526
527    #[test]
528    fn test_push_event() {
529        let mut tty = MockTty::new(80, 24);
530        assert_eq!(tty.queued_events(), 0);
531        tty.push_event(Event::Key(KeyEvent::new(
532            KeyCode::Enter,
533            KeyModifiers::NONE,
534        )));
535        assert_eq!(tty.queued_events(), 1);
536    }
537
538    #[test]
539    fn test_push_poll() {
540        let mut tty = MockTty::new(80, 24);
541        tty.push_poll(true);
542        assert!(tty.poll(Duration::ZERO).unwrap());
543    }
544
545    #[test]
546    fn test_set_size() {
547        let mut tty = MockTty::new(80, 24);
548        tty.set_size(120, 40);
549        assert_eq!(tty.size(), (120, 40));
550    }
551
552    #[test]
553    fn test_contains_escape() {
554        let mut tty = MockTty::new(80, 24);
555        tty.write_all(b"\x1b[2J").unwrap(); // Clear screen
556        assert!(tty.contains_escape("2J"));
557        assert!(!tty.contains_escape("0J"));
558    }
559
560    #[test]
561    fn test_parsed_commands_cursor_move() {
562        let mut tty = MockTty::new(80, 24);
563        tty.write_all(b"\x1b[10;20H").unwrap();
564        let commands = tty.parsed_commands();
565        assert_eq!(commands.len(), 1);
566        assert_eq!(commands[0], AnsiCommand::CursorMove { row: 10, col: 20 });
567    }
568
569    #[test]
570    fn test_parsed_commands_clear_screen() {
571        let mut tty = MockTty::new(80, 24);
572        tty.write_all(b"\x1b[2J").unwrap();
573        let commands = tty.parsed_commands();
574        assert_eq!(commands.len(), 1);
575        assert_eq!(commands[0], AnsiCommand::ClearScreen(ClearMode::All));
576    }
577
578    #[test]
579    fn test_parsed_commands_sgr() {
580        let mut tty = MockTty::new(80, 24);
581        tty.write_all(b"\x1b[1;31m").unwrap(); // Bold red
582        let commands = tty.parsed_commands();
583        assert_eq!(commands.len(), 1);
584        assert_eq!(commands[0], AnsiCommand::SetAttribute(vec![1, 31]));
585    }
586
587    #[test]
588    fn test_parsed_commands_text() {
589        let mut tty = MockTty::new(80, 24);
590        tty.write_all(b"Hello\x1b[2JWorld").unwrap();
591        let commands = tty.parsed_commands();
592        assert_eq!(commands.len(), 3);
593        assert_eq!(commands[0], AnsiCommand::Text("Hello".to_string()));
594        assert_eq!(commands[1], AnsiCommand::ClearScreen(ClearMode::All));
595        assert_eq!(commands[2], AnsiCommand::Text("World".to_string()));
596    }
597
598    #[test]
599    fn test_parsed_commands_alternate_screen() {
600        let mut tty = MockTty::new(80, 24);
601        tty.enter_alternate_screen();
602        tty.leave_alternate_screen();
603        let commands = tty.parsed_commands();
604        assert!(commands.contains(&AnsiCommand::EnterAlternateScreen));
605        assert!(commands.contains(&AnsiCommand::LeaveAlternateScreen));
606    }
607
608    #[test]
609    fn test_parsed_commands_cursor_visibility() {
610        let mut tty = MockTty::new(80, 24);
611        tty.hide_cursor();
612        tty.show_cursor();
613        let commands = tty.parsed_commands();
614        assert!(commands.contains(&AnsiCommand::HideCursor));
615        assert!(commands.contains(&AnsiCommand::ShowCursor));
616    }
617
618    #[test]
619    fn test_cursor_position_f_variant() {
620        // Test 'f' final byte (same as 'H' for cursor position)
621        let mut tty = MockTty::new(80, 24);
622        tty.write_all(b"\x1b[5;10f").unwrap();
623        let commands = tty.parsed_commands();
624        assert_eq!(commands.len(), 1);
625        assert_eq!(commands[0], AnsiCommand::CursorMove { row: 5, col: 10 });
626    }
627
628    #[test]
629    fn test_cursor_position_defaults() {
630        // Test cursor position with no params (defaults to 1,1)
631        let mut tty = MockTty::new(80, 24);
632        tty.write_all(b"\x1b[H").unwrap();
633        let commands = tty.parsed_commands();
634        assert_eq!(commands.len(), 1);
635        assert_eq!(commands[0], AnsiCommand::CursorMove { row: 1, col: 1 });
636    }
637
638    #[test]
639    fn test_cursor_position_row_only() {
640        // Test cursor position with only row (col defaults to 1)
641        let mut tty = MockTty::new(80, 24);
642        tty.write_all(b"\x1b[15H").unwrap();
643        let commands = tty.parsed_commands();
644        assert_eq!(commands.len(), 1);
645        assert_eq!(commands[0], AnsiCommand::CursorMove { row: 15, col: 1 });
646    }
647
648    #[test]
649    fn test_clear_screen_modes() {
650        let mut tty = MockTty::new(80, 24);
651        // ToEnd (default/0)
652        tty.write_all(b"\x1b[J").unwrap();
653        tty.write_all(b"\x1b[0J").unwrap();
654        // ToBeginning
655        tty.write_all(b"\x1b[1J").unwrap();
656        // All (both 2 and 3)
657        tty.write_all(b"\x1b[2J").unwrap();
658        tty.write_all(b"\x1b[3J").unwrap();
659        // Unknown param falls back to ToEnd
660        tty.write_all(b"\x1b[9J").unwrap();
661
662        let commands = tty.parsed_commands();
663        assert_eq!(commands.len(), 6);
664        assert_eq!(commands[0], AnsiCommand::ClearScreen(ClearMode::ToEnd));
665        assert_eq!(commands[1], AnsiCommand::ClearScreen(ClearMode::ToEnd));
666        assert_eq!(
667            commands[2],
668            AnsiCommand::ClearScreen(ClearMode::ToBeginning)
669        );
670        assert_eq!(commands[3], AnsiCommand::ClearScreen(ClearMode::All));
671        assert_eq!(commands[4], AnsiCommand::ClearScreen(ClearMode::All));
672        assert_eq!(commands[5], AnsiCommand::ClearScreen(ClearMode::ToEnd));
673    }
674
675    #[test]
676    fn test_clear_line_modes() {
677        let mut tty = MockTty::new(80, 24);
678        // ToEnd (default/0)
679        tty.write_all(b"\x1b[K").unwrap();
680        tty.write_all(b"\x1b[0K").unwrap();
681        // ToBeginning
682        tty.write_all(b"\x1b[1K").unwrap();
683        // All
684        tty.write_all(b"\x1b[2K").unwrap();
685        // Unknown param falls back to ToEnd
686        tty.write_all(b"\x1b[9K").unwrap();
687
688        let commands = tty.parsed_commands();
689        assert_eq!(commands.len(), 5);
690        assert_eq!(commands[0], AnsiCommand::ClearLine(ClearMode::ToEnd));
691        assert_eq!(commands[1], AnsiCommand::ClearLine(ClearMode::ToEnd));
692        assert_eq!(commands[2], AnsiCommand::ClearLine(ClearMode::ToBeginning));
693        assert_eq!(commands[3], AnsiCommand::ClearLine(ClearMode::All));
694        assert_eq!(commands[4], AnsiCommand::ClearLine(ClearMode::ToEnd));
695    }
696
697    #[test]
698    fn test_sgr_empty_params() {
699        let mut tty = MockTty::new(80, 24);
700        tty.write_all(b"\x1b[m").unwrap(); // Reset all attributes
701        let commands = tty.parsed_commands();
702        assert_eq!(commands.len(), 1);
703        assert_eq!(commands[0], AnsiCommand::SetAttribute(vec![]));
704    }
705
706    #[test]
707    fn test_unknown_h_mode() {
708        let mut tty = MockTty::new(80, 24);
709        tty.write_all(b"\x1b[?9999h").unwrap();
710        let commands = tty.parsed_commands();
711        assert_eq!(commands.len(), 1);
712        match &commands[0] {
713            AnsiCommand::Unknown(bytes) => {
714                assert_eq!(bytes, b"\x1b[?9999h");
715            }
716            _ => panic!("Expected Unknown command"),
717        }
718    }
719
720    #[test]
721    fn test_unknown_l_mode() {
722        let mut tty = MockTty::new(80, 24);
723        tty.write_all(b"\x1b[?9999l").unwrap();
724        let commands = tty.parsed_commands();
725        assert_eq!(commands.len(), 1);
726        match &commands[0] {
727            AnsiCommand::Unknown(bytes) => {
728                assert_eq!(bytes, b"\x1b[?9999l");
729            }
730            _ => panic!("Expected Unknown command"),
731        }
732    }
733
734    #[test]
735    fn test_unknown_final_byte() {
736        let mut tty = MockTty::new(80, 24);
737        // Use 'Z' which is not a recognized command
738        tty.write_all(b"\x1b[5Z").unwrap();
739        let commands = tty.parsed_commands();
740        assert_eq!(commands.len(), 1);
741        match &commands[0] {
742            AnsiCommand::Unknown(bytes) => {
743                assert_eq!(bytes, b"\x1b[5Z");
744            }
745            _ => panic!("Expected Unknown command"),
746        }
747    }
748
749    #[test]
750    fn test_mouse_enable_via_parsing() {
751        let mut tty = MockTty::new(80, 24);
752        tty.enable_mouse_capture();
753        let commands = tty.parsed_commands();
754        // Should contain EnableMouse (the first sequence ?1000h triggers it)
755        assert!(commands
756            .iter()
757            .any(|c| matches!(c, AnsiCommand::EnableMouse)));
758    }
759
760    #[test]
761    fn test_mouse_disable_via_parsing() {
762        let mut tty = MockTty::new(80, 24);
763        tty.disable_mouse_capture();
764        let commands = tty.parsed_commands();
765        // Should contain DisableMouse (the sequence ?1006l triggers it)
766        assert!(commands
767            .iter()
768            .any(|c| matches!(c, AnsiCommand::DisableMouse)));
769    }
770
771    #[test]
772    fn test_mouse_1002_enable() {
773        let mut tty = MockTty::new(80, 24);
774        tty.write_all(b"\x1b[?1002h").unwrap();
775        let commands = tty.parsed_commands();
776        assert_eq!(commands.len(), 1);
777        assert_eq!(commands[0], AnsiCommand::EnableMouse);
778    }
779
780    #[test]
781    fn test_mouse_1000_disable() {
782        let mut tty = MockTty::new(80, 24);
783        tty.write_all(b"\x1b[?1000l").unwrap();
784        let commands = tty.parsed_commands();
785        assert_eq!(commands.len(), 1);
786        assert_eq!(commands[0], AnsiCommand::DisableMouse);
787    }
788
789    #[test]
790    fn test_incomplete_escape_sequence() {
791        let mut tty = MockTty::new(80, 24);
792        // Escape sequence without final byte (ends at buffer end)
793        tty.write_all(b"text\x1b[123").unwrap();
794        let commands = tty.parsed_commands();
795        // Should have Text("text") and Unknown for the incomplete sequence
796        assert_eq!(commands.len(), 2);
797        assert_eq!(commands[0], AnsiCommand::Text("text".to_string()));
798        match &commands[1] {
799            AnsiCommand::Unknown(_) => {}
800            _ => panic!("Expected Unknown for incomplete sequence"),
801        }
802    }
803
804    #[test]
805    fn test_write_flush() {
806        let mut tty = MockTty::new(80, 24);
807        tty.write_all(b"test").unwrap();
808        // flush should always succeed for MockTty
809        assert!(tty.flush().is_ok());
810    }
811
812    #[test]
813    fn test_output_contains_empty_needle() {
814        let mut tty = MockTty::new(80, 24);
815        tty.write_all(b"Hello").unwrap();
816        // Empty needle should return false (windows(0) returns empty iterator)
817        assert!(!tty.output_contains(b""));
818    }
819
820    #[test]
821    fn test_intermediate_bytes_in_sequence() {
822        // Test that intermediate bytes (0x20-0x2F) are handled
823        let mut tty = MockTty::new(80, 24);
824        // CSI with intermediate byte (space) before final byte
825        tty.write_all(b"\x1b[0 q").unwrap(); // DECSCUSR - set cursor style
826        let commands = tty.parsed_commands();
827        assert_eq!(commands.len(), 1);
828        // Should be Unknown since 'q' with space is not recognized
829        match &commands[0] {
830            AnsiCommand::Unknown(_) => {}
831            _ => panic!("Expected Unknown command for DECSCUSR"),
832        }
833    }
834
835    #[test]
836    fn test_multiple_escape_sequences() {
837        let mut tty = MockTty::new(80, 24);
838        tty.write_all(b"\x1b[2J\x1b[1;1H\x1b[?25l").unwrap();
839        let commands = tty.parsed_commands();
840        assert_eq!(commands.len(), 3);
841        assert_eq!(commands[0], AnsiCommand::ClearScreen(ClearMode::All));
842        assert_eq!(commands[1], AnsiCommand::CursorMove { row: 1, col: 1 });
843        assert_eq!(commands[2], AnsiCommand::HideCursor);
844    }
845
846    #[test]
847    fn test_text_only_output() {
848        let mut tty = MockTty::new(80, 24);
849        tty.write_all(b"Just plain text").unwrap();
850        let commands = tty.parsed_commands();
851        assert_eq!(commands.len(), 1);
852        assert_eq!(
853            commands[0],
854            AnsiCommand::Text("Just plain text".to_string())
855        );
856    }
857
858    #[test]
859    fn test_escape_at_end() {
860        let mut tty = MockTty::new(80, 24);
861        // Just ESC without [ (not a CSI sequence)
862        tty.write_all(b"text\x1b").unwrap();
863        let commands = tty.parsed_commands();
864        // Should just be the text, ESC at end won't start a CSI
865        assert_eq!(commands.len(), 1);
866        assert_eq!(commands[0], AnsiCommand::Text("text\x1b".to_string()));
867    }
868
869    #[test]
870    fn test_debug_impl() {
871        let tty = MockTty::new(80, 24);
872        let debug_str = format!("{:?}", tty);
873        assert!(debug_str.contains("MockTty"));
874        assert!(debug_str.contains("size"));
875    }
876
877    #[test]
878    fn test_ansi_command_debug_and_clone() {
879        let cmd = AnsiCommand::CursorMove { row: 5, col: 10 };
880        let cloned = cmd.clone();
881        assert_eq!(cmd, cloned);
882        let debug_str = format!("{:?}", cmd);
883        assert!(debug_str.contains("CursorMove"));
884    }
885
886    #[test]
887    fn test_clear_mode_debug_and_clone() {
888        let mode = ClearMode::All;
889        let cloned = mode;
890        assert_eq!(mode, cloned);
891        let debug_str = format!("{:?}", mode);
892        assert!(debug_str.contains("All"));
893    }
894
895    #[test]
896    fn test_empty_output_parsing() {
897        let tty = MockTty::new(80, 24);
898        let commands = tty.parsed_commands();
899        assert!(commands.is_empty());
900    }
901
902    #[test]
903    fn test_read_event_error_kind() {
904        let mut tty = MockTty::new(80, 24);
905        let err = tty.read_event().unwrap_err();
906        assert_eq!(err.kind(), io::ErrorKind::WouldBlock);
907    }
908}