git_branchless_undo/tui/
testing.rs

1//! Testing helpers for interactive interfaces.
2
3use std::borrow::Borrow;
4use std::cell::RefCell;
5use std::rc::Rc;
6
7use cursive::backend::Backend;
8use cursive::theme::Color;
9
10/// Represents a "screenshot" of the terminal taken at a point in time.
11pub type Screen = Vec<Vec<char>>;
12
13/// The kind of events that can be
14#[derive(Clone, Debug)]
15pub enum CursiveTestingEvent {
16    /// A regular Cursive event.
17    Event(cursive::event::Event),
18
19    /// Take a screenshot at the current point in time and store it in the
20    /// provided screenshot cell.
21    TakeScreenshot(Rc<RefCell<Screen>>),
22}
23
24/// The testing backend. It feeds a predetermined list of events to the
25/// Cursive event loop and stores a virtual terminal for Cursive to draw on.
26#[derive(Debug)]
27pub struct CursiveTestingBackend {
28    events: Vec<CursiveTestingEvent>,
29    event_index: usize,
30    just_emitted_event: bool,
31    screen: RefCell<Screen>,
32}
33
34impl CursiveTestingBackend {
35    /// Construct the testing backend with the provided set of events.
36    pub fn init(events: Vec<CursiveTestingEvent>) -> Box<dyn Backend> {
37        Box::new(CursiveTestingBackend {
38            events,
39            event_index: 0,
40            just_emitted_event: false,
41            screen: RefCell::new(vec![vec![' '; 120]; 24]),
42        })
43    }
44}
45
46impl Backend for CursiveTestingBackend {
47    fn poll_event(&mut self) -> Option<cursive::event::Event> {
48        // Cursive will poll all available events. We only want it to
49        // process events one at a time, so return `None` after each event.
50        if self.just_emitted_event {
51            self.just_emitted_event = false;
52            return None;
53        }
54
55        let event_index = self.event_index;
56        self.event_index += 1;
57        match self.events.get(event_index)?.to_owned() {
58            CursiveTestingEvent::TakeScreenshot(screen_target) => {
59                let mut screen_target = (*screen_target).borrow_mut();
60                screen_target.clone_from(&self.screen.borrow());
61                self.poll_event()
62            }
63            CursiveTestingEvent::Event(event) => {
64                self.just_emitted_event = true;
65                Some(event)
66            }
67        }
68    }
69
70    fn refresh(&mut self) {}
71
72    fn has_colors(&self) -> bool {
73        false
74    }
75
76    fn screen_size(&self) -> cursive::Vec2 {
77        let screen = self.screen.borrow();
78        (screen[0].len(), screen.len()).into()
79    }
80
81    fn print_at(&self, pos: cursive::Vec2, text: &str) {
82        for (i, c) in text.chars().enumerate() {
83            let mut screen = self.screen.borrow_mut();
84            let screen_width = screen[0].len();
85            if pos.x + i < screen_width {
86                screen[pos.y][pos.x + i] = c;
87            } else {
88                // Indicate that the screen was overfull.
89                screen[pos.y][screen_width - 1] = '$';
90            }
91        }
92    }
93
94    fn clear(&self, _color: Color) {
95        let mut screen = self.screen.borrow_mut();
96        for i in 0..screen.len() {
97            for j in 0..screen[i].len() {
98                screen[i][j] = ' ';
99            }
100        }
101    }
102
103    fn set_color(&self, colors: cursive::theme::ColorPair) -> cursive::theme::ColorPair {
104        colors
105    }
106
107    fn set_effect(&self, _effect: cursive::theme::Effect) {}
108
109    fn unset_effect(&self, _effect: cursive::theme::Effect) {}
110
111    fn set_title(&mut self, _title: String) {}
112}
113
114/// Convert the screenshot into a string for assertions, such as for use
115/// with `insta::assert_snapshot!`.
116pub fn screen_to_string(screen: &Rc<RefCell<Screen>>) -> String {
117    let screen = Rc::borrow(screen);
118    let screen = RefCell::borrow(screen);
119    screen
120        .iter()
121        .map(|row| {
122            let line: String = row.iter().collect();
123            line.trim_end().to_owned() + "\n"
124        })
125        .collect::<String>()
126        .trim()
127        .to_owned()
128}