Skip to main content

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    cursor_pos: RefCell<cursive::Vec2>,
33}
34
35impl CursiveTestingBackend {
36    /// Construct the testing backend with the provided set of events.
37    pub fn init(events: Vec<CursiveTestingEvent>) -> Box<dyn Backend> {
38        Box::new(CursiveTestingBackend {
39            events,
40            event_index: 0,
41            just_emitted_event: false,
42            screen: RefCell::new(vec![vec![' '; 120]; 24]),
43            cursor_pos: RefCell::new(cursive::Vec2::zero()),
44        })
45    }
46}
47
48impl Backend for CursiveTestingBackend {
49    fn poll_event(&mut self) -> Option<cursive::event::Event> {
50        // Cursive will poll all available events. We only want it to
51        // process events one at a time, so return `None` after each event.
52        if self.just_emitted_event {
53            self.just_emitted_event = false;
54            return None;
55        }
56
57        let event_index = self.event_index;
58        self.event_index += 1;
59        match self.events.get(event_index)?.to_owned() {
60            CursiveTestingEvent::TakeScreenshot(screen_target) => {
61                let mut screen_target = (*screen_target).borrow_mut();
62                screen_target.clone_from(&self.screen.borrow());
63                self.poll_event()
64            }
65            CursiveTestingEvent::Event(event) => {
66                self.just_emitted_event = true;
67                Some(event)
68            }
69        }
70    }
71
72    fn refresh(&mut self) {}
73
74    fn has_colors(&self) -> bool {
75        false
76    }
77
78    fn screen_size(&self) -> cursive::Vec2 {
79        let screen = self.screen.borrow();
80        (screen[0].len(), screen.len()).into()
81    }
82
83    fn move_to(&self, pos: cursive::Vec2) {
84        *self.cursor_pos.borrow_mut() = pos;
85    }
86
87    fn print(&self, text: &str) {
88        let pos = *self.cursor_pos.borrow();
89        let mut col = pos.x;
90        for c in text.chars() {
91            let mut screen = self.screen.borrow_mut();
92            let screen_width = screen[0].len();
93            if col < screen_width {
94                screen[pos.y][col] = c;
95                col += 1;
96            } else {
97                // Indicate that the screen was overfull.
98                screen[pos.y][screen_width - 1] = '$';
99                break;
100            }
101        }
102        self.cursor_pos.borrow_mut().x = col;
103    }
104
105    fn clear(&self, _color: Color) {
106        let mut screen = self.screen.borrow_mut();
107        for i in 0..screen.len() {
108            for j in 0..screen[i].len() {
109                screen[i][j] = ' ';
110            }
111        }
112    }
113
114    fn set_color(&self, colors: cursive::theme::ColorPair) -> cursive::theme::ColorPair {
115        colors
116    }
117
118    fn set_effect(&self, _effect: cursive::theme::Effect) {}
119
120    fn unset_effect(&self, _effect: cursive::theme::Effect) {}
121
122    fn set_title(&mut self, _title: String) {}
123}
124
125/// Convert the screenshot into a string for assertions, such as for use
126/// with `insta::assert_snapshot!`.
127pub fn screen_to_string(screen: &Rc<RefCell<Screen>>) -> String {
128    let screen = Rc::borrow(screen);
129    let screen = RefCell::borrow(screen);
130    screen
131        .iter()
132        .map(|row| {
133            let line: String = row.iter().collect();
134            line.trim_end().to_owned() + "\n"
135        })
136        .collect::<String>()
137        .trim()
138        .to_owned()
139}