ratatui/backend/
test.rs

1//! This module provides the `TestBackend` implementation for the [`Backend`] trait.
2//! It is used in the integration tests to verify the correctness of the library.
3
4use std::{
5    fmt::{self, Write},
6    io, iter,
7};
8
9use unicode_width::UnicodeWidthStr;
10
11use crate::{
12    backend::{Backend, ClearType, WindowSize},
13    buffer::{Buffer, Cell},
14    layout::{Position, Rect, Size},
15};
16
17/// A [`Backend`] implementation used for integration testing that renders to an memory buffer.
18///
19/// Note: that although many of the integration and unit tests in ratatui are written using this
20/// backend, it is preferable to write unit tests for widgets directly against the buffer rather
21/// than using this backend. This backend is intended for integration tests that test the entire
22/// terminal UI.
23///
24/// # Example
25///
26/// ```rust
27/// use ratatui::backend::{Backend, TestBackend};
28///
29/// let mut backend = TestBackend::new(10, 2);
30/// backend.clear()?;
31/// backend.assert_buffer_lines(["          "; 2]);
32/// # std::io::Result::Ok(())
33/// ```
34#[derive(Debug, Clone, Eq, PartialEq, Hash)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub struct TestBackend {
37    buffer: Buffer,
38    scrollback: Buffer,
39    cursor: bool,
40    pos: (u16, u16),
41}
42
43/// Returns a string representation of the given buffer for debugging purpose.
44///
45/// This function is used to visualize the buffer content in a human-readable format.
46/// It iterates through the buffer content and appends each cell's symbol to the view string.
47/// If a cell is hidden by a multi-width symbol, it is added to the overwritten vector and
48/// displayed at the end of the line.
49fn buffer_view(buffer: &Buffer) -> String {
50    let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3);
51    for cells in buffer.content.chunks(buffer.area.width as usize) {
52        let mut overwritten = vec![];
53        let mut skip: usize = 0;
54        view.push('"');
55        for (x, c) in cells.iter().enumerate() {
56            if skip == 0 {
57                view.push_str(c.symbol());
58            } else {
59                overwritten.push((x, c.symbol()));
60            }
61            skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
62        }
63        view.push('"');
64        if !overwritten.is_empty() {
65            write!(&mut view, " Hidden by multi-width symbols: {overwritten:?}").unwrap();
66        }
67        view.push('\n');
68    }
69    view
70}
71
72impl TestBackend {
73    /// Creates a new `TestBackend` with the specified width and height.
74    pub fn new(width: u16, height: u16) -> Self {
75        Self {
76            buffer: Buffer::empty(Rect::new(0, 0, width, height)),
77            scrollback: Buffer::empty(Rect::new(0, 0, width, 0)),
78            cursor: false,
79            pos: (0, 0),
80        }
81    }
82
83    /// Creates a new `TestBackend` with the specified lines as the initial screen state.
84    ///
85    /// The backend's screen size is determined from the initial lines.
86    #[must_use]
87    pub fn with_lines<'line, Lines>(lines: Lines) -> Self
88    where
89        Lines: IntoIterator,
90        Lines::Item: Into<crate::text::Line<'line>>,
91    {
92        let buffer = Buffer::with_lines(lines);
93        let scrollback = Buffer::empty(Rect {
94            width: buffer.area.width,
95            ..Rect::ZERO
96        });
97        Self {
98            buffer,
99            scrollback,
100            cursor: false,
101            pos: (0, 0),
102        }
103    }
104
105    /// Returns a reference to the internal buffer of the `TestBackend`.
106    pub const fn buffer(&self) -> &Buffer {
107        &self.buffer
108    }
109
110    /// Returns a reference to the internal scrollback buffer of the `TestBackend`.
111    ///
112    /// The scrollback buffer represents the part of the screen that is currently hidden from view,
113    /// but that could be accessed by scrolling back in the terminal's history. This would normally
114    /// be done using the terminal's scrollbar or an equivalent keyboard shortcut.
115    ///
116    /// The scrollback buffer starts out empty. Lines are appended when they scroll off the top of
117    /// the main buffer. This happens when lines are appended to the bottom of the main buffer
118    /// using [`Backend::append_lines`].
119    ///
120    /// The scrollback buffer has a maximum height of [`u16::MAX`]. If lines are appended to the
121    /// bottom of the scrollback buffer when it is at its maximum height, a corresponding number of
122    /// lines will be removed from the top.
123    pub const fn scrollback(&self) -> &Buffer {
124        &self.scrollback
125    }
126
127    /// Resizes the `TestBackend` to the specified width and height.
128    pub fn resize(&mut self, width: u16, height: u16) {
129        self.buffer.resize(Rect::new(0, 0, width, height));
130        let scrollback_height = self.scrollback.area.height;
131        self.scrollback
132            .resize(Rect::new(0, 0, width, scrollback_height));
133    }
134
135    /// Asserts that the `TestBackend`'s buffer is equal to the expected buffer.
136    ///
137    /// This is a shortcut for `assert_eq!(self.buffer(), &expected)`.
138    ///
139    /// # Panics
140    ///
141    /// When they are not equal, a panic occurs with a detailed error message showing the
142    /// differences between the expected and actual buffers.
143    #[allow(deprecated)]
144    #[track_caller]
145    pub fn assert_buffer(&self, expected: &Buffer) {
146        // TODO: use assert_eq!()
147        crate::assert_buffer_eq!(&self.buffer, expected);
148    }
149
150    /// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected buffer.
151    ///
152    /// This is a shortcut for `assert_eq!(self.scrollback(), &expected)`.
153    ///
154    /// # Panics
155    ///
156    /// When they are not equal, a panic occurs with a detailed error message showing the
157    /// differences between the expected and actual buffers.
158    #[track_caller]
159    pub fn assert_scrollback(&self, expected: &Buffer) {
160        assert_eq!(&self.scrollback, expected);
161    }
162
163    /// Asserts that the `TestBackend`'s scrollback buffer is empty.
164    ///
165    /// # Panics
166    ///
167    /// When the scrollback buffer is not equal, a panic occurs with a detailed error message
168    /// showing the differences between the expected and actual buffers.
169    pub fn assert_scrollback_empty(&self) {
170        let expected = Buffer {
171            area: Rect {
172                width: self.scrollback.area.width,
173                ..Rect::ZERO
174            },
175            content: vec![],
176        };
177        self.assert_scrollback(&expected);
178    }
179
180    /// Asserts that the `TestBackend`'s buffer is equal to the expected lines.
181    ///
182    /// This is a shortcut for `assert_eq!(self.buffer(), &Buffer::with_lines(expected))`.
183    ///
184    /// # Panics
185    ///
186    /// When they are not equal, a panic occurs with a detailed error message showing the
187    /// differences between the expected and actual buffers.
188    #[track_caller]
189    pub fn assert_buffer_lines<'line, Lines>(&self, expected: Lines)
190    where
191        Lines: IntoIterator,
192        Lines::Item: Into<crate::text::Line<'line>>,
193    {
194        self.assert_buffer(&Buffer::with_lines(expected));
195    }
196
197    /// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected lines.
198    ///
199    /// This is a shortcut for `assert_eq!(self.scrollback(), &Buffer::with_lines(expected))`.
200    ///
201    /// # Panics
202    ///
203    /// When they are not equal, a panic occurs with a detailed error message showing the
204    /// differences between the expected and actual buffers.
205    #[track_caller]
206    pub fn assert_scrollback_lines<'line, Lines>(&self, expected: Lines)
207    where
208        Lines: IntoIterator,
209        Lines::Item: Into<crate::text::Line<'line>>,
210    {
211        self.assert_scrollback(&Buffer::with_lines(expected));
212    }
213
214    /// Asserts that the `TestBackend`'s cursor position is equal to the expected one.
215    ///
216    /// This is a shortcut for `assert_eq!(self.get_cursor_position().unwrap(), expected)`.
217    ///
218    /// # Panics
219    ///
220    /// When they are not equal, a panic occurs with a detailed error message showing the
221    /// differences between the expected and actual position.
222    #[track_caller]
223    pub fn assert_cursor_position<P: Into<Position>>(&mut self, position: P) {
224        let actual = self.get_cursor_position().unwrap();
225        assert_eq!(actual, position.into());
226    }
227}
228
229impl fmt::Display for TestBackend {
230    /// Formats the `TestBackend` for display by calling the `buffer_view` function
231    /// on its internal buffer.
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        write!(f, "{}", buffer_view(&self.buffer))
234    }
235}
236
237impl Backend for TestBackend {
238    fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
239    where
240        I: Iterator<Item = (u16, u16, &'a Cell)>,
241    {
242        for (x, y, c) in content {
243            self.buffer[(x, y)] = c.clone();
244        }
245        Ok(())
246    }
247
248    fn hide_cursor(&mut self) -> io::Result<()> {
249        self.cursor = false;
250        Ok(())
251    }
252
253    fn show_cursor(&mut self) -> io::Result<()> {
254        self.cursor = true;
255        Ok(())
256    }
257
258    fn get_cursor_position(&mut self) -> io::Result<Position> {
259        Ok(self.pos.into())
260    }
261
262    fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
263        self.pos = position.into().into();
264        Ok(())
265    }
266
267    fn clear(&mut self) -> io::Result<()> {
268        self.buffer.reset();
269        Ok(())
270    }
271
272    fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
273        let region = match clear_type {
274            ClearType::All => return self.clear(),
275            ClearType::AfterCursor => {
276                let index = self.buffer.index_of(self.pos.0, self.pos.1) + 1;
277                &mut self.buffer.content[index..]
278            }
279            ClearType::BeforeCursor => {
280                let index = self.buffer.index_of(self.pos.0, self.pos.1);
281                &mut self.buffer.content[..index]
282            }
283            ClearType::CurrentLine => {
284                let line_start_index = self.buffer.index_of(0, self.pos.1);
285                let line_end_index = self.buffer.index_of(self.buffer.area.width - 1, self.pos.1);
286                &mut self.buffer.content[line_start_index..=line_end_index]
287            }
288            ClearType::UntilNewLine => {
289                let index = self.buffer.index_of(self.pos.0, self.pos.1);
290                let line_end_index = self.buffer.index_of(self.buffer.area.width - 1, self.pos.1);
291                &mut self.buffer.content[index..=line_end_index]
292            }
293        };
294        for cell in region {
295            cell.reset();
296        }
297        Ok(())
298    }
299
300    /// Inserts n line breaks at the current cursor position.
301    ///
302    /// After the insertion, the cursor x position will be incremented by 1 (unless it's already
303    /// at the end of line). This is a common behaviour of terminals in raw mode.
304    ///
305    /// If the number of lines to append is fewer than the number of lines in the buffer after the
306    /// cursor y position then the cursor is moved down by n rows.
307    ///
308    /// If the number of lines to append is greater than the number of lines in the buffer after
309    /// the cursor y position then that number of empty lines (at most the buffer's height in this
310    /// case but this limit is instead replaced with scrolling in most backend implementations) will
311    /// be added after the current position and the cursor will be moved to the last row.
312    fn append_lines(&mut self, line_count: u16) -> io::Result<()> {
313        let Position { x: cur_x, y: cur_y } = self.get_cursor_position()?;
314        let Rect { width, height, .. } = self.buffer.area;
315
316        // the next column ensuring that we don't go past the last column
317        let new_cursor_x = cur_x.saturating_add(1).min(width.saturating_sub(1));
318
319        let max_y = height.saturating_sub(1);
320        let lines_after_cursor = max_y.saturating_sub(cur_y);
321
322        if line_count > lines_after_cursor {
323            // We need to insert blank lines at the bottom and scroll the lines from the top into
324            // scrollback.
325            let scroll_by: usize = (line_count - lines_after_cursor).into();
326            let width: usize = self.buffer.area.width.into();
327            let cells_to_scrollback = self.buffer.content.len().min(width * scroll_by);
328
329            append_to_scrollback(
330                &mut self.scrollback,
331                self.buffer.content.splice(
332                    0..cells_to_scrollback,
333                    iter::repeat_with(Default::default).take(cells_to_scrollback),
334                ),
335            );
336            self.buffer.content.rotate_left(cells_to_scrollback);
337            append_to_scrollback(
338                &mut self.scrollback,
339                iter::repeat_with(Default::default).take(width * scroll_by - cells_to_scrollback),
340            );
341        }
342
343        let new_cursor_y = cur_y.saturating_add(line_count).min(max_y);
344        self.set_cursor_position(Position::new(new_cursor_x, new_cursor_y))?;
345
346        Ok(())
347    }
348
349    fn size(&self) -> io::Result<Size> {
350        Ok(self.buffer.area.as_size())
351    }
352
353    fn window_size(&mut self) -> io::Result<WindowSize> {
354        // Some arbitrary window pixel size, probably doesn't need much testing.
355        const WINDOW_PIXEL_SIZE: Size = Size {
356            width: 640,
357            height: 480,
358        };
359        Ok(WindowSize {
360            columns_rows: self.buffer.area.as_size(),
361            pixels: WINDOW_PIXEL_SIZE,
362        })
363    }
364
365    fn flush(&mut self) -> io::Result<()> {
366        Ok(())
367    }
368
369    #[cfg(feature = "scrolling-regions")]
370    fn scroll_region_up(&mut self, region: std::ops::Range<u16>, scroll_by: u16) -> io::Result<()> {
371        let width: usize = self.buffer.area.width.into();
372        let cell_region_start = width * region.start.min(self.buffer.area.height) as usize;
373        let cell_region_end = width * region.end.min(self.buffer.area.height) as usize;
374        let cell_region_len = cell_region_end - cell_region_start;
375        let cells_to_scroll_by = width * scroll_by as usize;
376
377        // Deal with the simple case where nothing needs to be copied into scrollback.
378        if cell_region_start > 0 {
379            if cells_to_scroll_by >= cell_region_len {
380                // The scroll amount is large enough to clear the whole region.
381                self.buffer.content[cell_region_start..cell_region_end].fill_with(Default::default);
382            } else {
383                // Scroll up by rotating, then filling in the bottom with empty cells.
384                self.buffer.content[cell_region_start..cell_region_end]
385                    .rotate_left(cells_to_scroll_by);
386                self.buffer.content[cell_region_end - cells_to_scroll_by..cell_region_end]
387                    .fill_with(Default::default);
388            }
389            return Ok(());
390        }
391
392        // The rows inserted into the scrollback will first come from the buffer, and if that is
393        // insufficient, will then be blank rows.
394        let cells_from_region = cell_region_len.min(cells_to_scroll_by);
395        append_to_scrollback(
396            &mut self.scrollback,
397            self.buffer.content.splice(
398                0..cells_from_region,
399                iter::repeat_with(Default::default).take(cells_from_region),
400            ),
401        );
402        if cells_to_scroll_by < cell_region_len {
403            // Rotate the remaining cells to the front of the region.
404            self.buffer.content[cell_region_start..cell_region_end].rotate_left(cells_from_region);
405        } else {
406            // Splice cleared out the region. Insert empty rows in scrollback.
407            append_to_scrollback(
408                &mut self.scrollback,
409                iter::repeat_with(Default::default).take(cells_to_scroll_by - cell_region_len),
410            );
411        }
412        Ok(())
413    }
414
415    #[cfg(feature = "scrolling-regions")]
416    fn scroll_region_down(
417        &mut self,
418        region: std::ops::Range<u16>,
419        scroll_by: u16,
420    ) -> io::Result<()> {
421        let width: usize = self.buffer.area.width.into();
422        let cell_region_start = width * region.start.min(self.buffer.area.height) as usize;
423        let cell_region_end = width * region.end.min(self.buffer.area.height) as usize;
424        let cell_region_len = cell_region_end - cell_region_start;
425        let cells_to_scroll_by = width * scroll_by as usize;
426
427        if cells_to_scroll_by >= cell_region_len {
428            // The scroll amount is large enough to clear the whole region.
429            self.buffer.content[cell_region_start..cell_region_end].fill_with(Default::default);
430        } else {
431            // Scroll up by rotating, then filling in the top with empty cells.
432            self.buffer.content[cell_region_start..cell_region_end]
433                .rotate_right(cells_to_scroll_by);
434            self.buffer.content[cell_region_start..cell_region_start + cells_to_scroll_by]
435                .fill_with(Default::default);
436        }
437        Ok(())
438    }
439}
440
441/// Append the provided cells to the bottom of a scrollback buffer. The number of cells must be a
442/// multiple of the buffer's width. If the scrollback buffer ends up larger than 65535 lines tall,
443/// then lines will be removed from the top to get it down to size.
444fn append_to_scrollback(scrollback: &mut Buffer, cells: impl IntoIterator<Item = Cell>) {
445    scrollback.content.extend(cells);
446    let width = scrollback.area.width as usize;
447    let new_height = (scrollback.content.len() / width).min(u16::MAX as usize);
448    let keep_from = scrollback
449        .content
450        .len()
451        .saturating_sub(width * u16::MAX as usize);
452    scrollback.content.drain(0..keep_from);
453    scrollback.area.height = new_height as u16;
454}
455
456#[cfg(test)]
457mod tests {
458    use itertools::Itertools as _;
459
460    use super::*;
461
462    #[test]
463    fn new() {
464        assert_eq!(
465            TestBackend::new(10, 2),
466            TestBackend {
467                buffer: Buffer::with_lines(["          "; 2]),
468                scrollback: Buffer::empty(Rect::new(0, 0, 10, 0)),
469                cursor: false,
470                pos: (0, 0),
471            }
472        );
473    }
474    #[test]
475    fn test_buffer_view() {
476        let buffer = Buffer::with_lines(["aaaa"; 2]);
477        assert_eq!(buffer_view(&buffer), "\"aaaa\"\n\"aaaa\"\n");
478    }
479
480    #[test]
481    fn buffer_view_with_overwrites() {
482        let multi_byte_char = "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ"; // renders 2 wide
483        let buffer = Buffer::with_lines([multi_byte_char]);
484        assert_eq!(
485            buffer_view(&buffer),
486            format!(
487                r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " ")]
488"#,
489            )
490        );
491    }
492
493    #[test]
494    fn buffer() {
495        let backend = TestBackend::new(10, 2);
496        backend.assert_buffer_lines(["          "; 2]);
497    }
498
499    #[test]
500    fn resize() {
501        let mut backend = TestBackend::new(10, 2);
502        backend.resize(5, 5);
503        backend.assert_buffer_lines(["     "; 5]);
504    }
505
506    #[test]
507    fn assert_buffer() {
508        let backend = TestBackend::new(10, 2);
509        backend.assert_buffer_lines(["          "; 2]);
510    }
511
512    #[test]
513    #[should_panic = "buffer contents not equal"]
514    fn assert_buffer_panics() {
515        let backend = TestBackend::new(10, 2);
516        backend.assert_buffer_lines(["aaaaaaaaaa"; 2]);
517    }
518
519    #[test]
520    #[should_panic = "assertion `left == right` failed"]
521    fn assert_scrollback_panics() {
522        let backend = TestBackend::new(10, 2);
523        backend.assert_scrollback_lines(["aaaaaaaaaa"; 2]);
524    }
525
526    #[test]
527    fn display() {
528        let backend = TestBackend::new(10, 2);
529        assert_eq!(format!("{backend}"), "\"          \"\n\"          \"\n");
530    }
531
532    #[test]
533    fn draw() {
534        let mut backend = TestBackend::new(10, 2);
535        let cell = Cell::new("a");
536        backend.draw([(0, 0, &cell)].into_iter()).unwrap();
537        backend.draw([(0, 1, &cell)].into_iter()).unwrap();
538        backend.assert_buffer_lines(["a         "; 2]);
539    }
540
541    #[test]
542    fn hide_cursor() {
543        let mut backend = TestBackend::new(10, 2);
544        backend.hide_cursor().unwrap();
545        assert!(!backend.cursor);
546    }
547
548    #[test]
549    fn show_cursor() {
550        let mut backend = TestBackend::new(10, 2);
551        backend.show_cursor().unwrap();
552        assert!(backend.cursor);
553    }
554
555    #[test]
556    fn get_cursor_position() {
557        let mut backend = TestBackend::new(10, 2);
558        assert_eq!(backend.get_cursor_position().unwrap(), Position::ORIGIN);
559    }
560
561    #[test]
562    fn assert_cursor_position() {
563        let mut backend = TestBackend::new(10, 2);
564        backend.assert_cursor_position(Position::ORIGIN);
565    }
566
567    #[test]
568    fn set_cursor_position() {
569        let mut backend = TestBackend::new(10, 10);
570        backend
571            .set_cursor_position(Position { x: 5, y: 5 })
572            .unwrap();
573        assert_eq!(backend.pos, (5, 5));
574    }
575
576    #[test]
577    fn clear() {
578        let mut backend = TestBackend::new(4, 2);
579        let cell = Cell::new("a");
580        backend.draw([(0, 0, &cell)].into_iter()).unwrap();
581        backend.draw([(0, 1, &cell)].into_iter()).unwrap();
582        backend.clear().unwrap();
583        backend.assert_buffer_lines(["    ", "    "]);
584    }
585
586    #[test]
587    fn clear_region_all() {
588        let mut backend = TestBackend::with_lines([
589            "aaaaaaaaaa",
590            "aaaaaaaaaa",
591            "aaaaaaaaaa",
592            "aaaaaaaaaa",
593            "aaaaaaaaaa",
594        ]);
595
596        backend.clear_region(ClearType::All).unwrap();
597        backend.assert_buffer_lines([
598            "          ",
599            "          ",
600            "          ",
601            "          ",
602            "          ",
603        ]);
604    }
605
606    #[test]
607    fn clear_region_after_cursor() {
608        let mut backend = TestBackend::with_lines([
609            "aaaaaaaaaa",
610            "aaaaaaaaaa",
611            "aaaaaaaaaa",
612            "aaaaaaaaaa",
613            "aaaaaaaaaa",
614        ]);
615
616        backend
617            .set_cursor_position(Position { x: 3, y: 2 })
618            .unwrap();
619        backend.clear_region(ClearType::AfterCursor).unwrap();
620        backend.assert_buffer_lines([
621            "aaaaaaaaaa",
622            "aaaaaaaaaa",
623            "aaaa      ",
624            "          ",
625            "          ",
626        ]);
627    }
628
629    #[test]
630    fn clear_region_before_cursor() {
631        let mut backend = TestBackend::with_lines([
632            "aaaaaaaaaa",
633            "aaaaaaaaaa",
634            "aaaaaaaaaa",
635            "aaaaaaaaaa",
636            "aaaaaaaaaa",
637        ]);
638
639        backend
640            .set_cursor_position(Position { x: 5, y: 3 })
641            .unwrap();
642        backend.clear_region(ClearType::BeforeCursor).unwrap();
643        backend.assert_buffer_lines([
644            "          ",
645            "          ",
646            "          ",
647            "     aaaaa",
648            "aaaaaaaaaa",
649        ]);
650    }
651
652    #[test]
653    fn clear_region_current_line() {
654        let mut backend = TestBackend::with_lines([
655            "aaaaaaaaaa",
656            "aaaaaaaaaa",
657            "aaaaaaaaaa",
658            "aaaaaaaaaa",
659            "aaaaaaaaaa",
660        ]);
661
662        backend
663            .set_cursor_position(Position { x: 3, y: 1 })
664            .unwrap();
665        backend.clear_region(ClearType::CurrentLine).unwrap();
666        backend.assert_buffer_lines([
667            "aaaaaaaaaa",
668            "          ",
669            "aaaaaaaaaa",
670            "aaaaaaaaaa",
671            "aaaaaaaaaa",
672        ]);
673    }
674
675    #[test]
676    fn clear_region_until_new_line() {
677        let mut backend = TestBackend::with_lines([
678            "aaaaaaaaaa",
679            "aaaaaaaaaa",
680            "aaaaaaaaaa",
681            "aaaaaaaaaa",
682            "aaaaaaaaaa",
683        ]);
684
685        backend
686            .set_cursor_position(Position { x: 3, y: 0 })
687            .unwrap();
688        backend.clear_region(ClearType::UntilNewLine).unwrap();
689        backend.assert_buffer_lines([
690            "aaa       ",
691            "aaaaaaaaaa",
692            "aaaaaaaaaa",
693            "aaaaaaaaaa",
694            "aaaaaaaaaa",
695        ]);
696    }
697
698    #[test]
699    fn append_lines_not_at_last_line() {
700        let mut backend = TestBackend::with_lines([
701            "aaaaaaaaaa",
702            "bbbbbbbbbb",
703            "cccccccccc",
704            "dddddddddd",
705            "eeeeeeeeee",
706        ]);
707
708        backend.set_cursor_position(Position::ORIGIN).unwrap();
709
710        // If the cursor is not at the last line in the terminal the addition of a
711        // newline simply moves the cursor down and to the right
712
713        backend.append_lines(1).unwrap();
714        backend.assert_cursor_position(Position { x: 1, y: 1 });
715
716        backend.append_lines(1).unwrap();
717        backend.assert_cursor_position(Position { x: 2, y: 2 });
718
719        backend.append_lines(1).unwrap();
720        backend.assert_cursor_position(Position { x: 3, y: 3 });
721
722        backend.append_lines(1).unwrap();
723        backend.assert_cursor_position(Position { x: 4, y: 4 });
724
725        // As such the buffer should remain unchanged
726        backend.assert_buffer_lines([
727            "aaaaaaaaaa",
728            "bbbbbbbbbb",
729            "cccccccccc",
730            "dddddddddd",
731            "eeeeeeeeee",
732        ]);
733        backend.assert_scrollback_empty();
734    }
735
736    #[test]
737    fn append_lines_at_last_line() {
738        let mut backend = TestBackend::with_lines([
739            "aaaaaaaaaa",
740            "bbbbbbbbbb",
741            "cccccccccc",
742            "dddddddddd",
743            "eeeeeeeeee",
744        ]);
745
746        // If the cursor is at the last line in the terminal the addition of a
747        // newline will scroll the contents of the buffer
748        backend
749            .set_cursor_position(Position { x: 0, y: 4 })
750            .unwrap();
751
752        backend.append_lines(1).unwrap();
753
754        backend.assert_buffer_lines([
755            "bbbbbbbbbb",
756            "cccccccccc",
757            "dddddddddd",
758            "eeeeeeeeee",
759            "          ",
760        ]);
761        backend.assert_scrollback_lines(["aaaaaaaaaa"]);
762
763        // It also moves the cursor to the right, as is common of the behaviour of
764        // terminals in raw-mode
765        backend.assert_cursor_position(Position { x: 1, y: 4 });
766    }
767
768    #[test]
769    fn append_multiple_lines_not_at_last_line() {
770        let mut backend = TestBackend::with_lines([
771            "aaaaaaaaaa",
772            "bbbbbbbbbb",
773            "cccccccccc",
774            "dddddddddd",
775            "eeeeeeeeee",
776        ]);
777
778        backend.set_cursor_position(Position::ORIGIN).unwrap();
779
780        // If the cursor is not at the last line in the terminal the addition of multiple
781        // newlines simply moves the cursor n lines down and to the right by 1
782
783        backend.append_lines(4).unwrap();
784        backend.assert_cursor_position(Position { x: 1, y: 4 });
785
786        // As such the buffer should remain unchanged
787        backend.assert_buffer_lines([
788            "aaaaaaaaaa",
789            "bbbbbbbbbb",
790            "cccccccccc",
791            "dddddddddd",
792            "eeeeeeeeee",
793        ]);
794        backend.assert_scrollback_empty();
795    }
796
797    #[test]
798    fn append_multiple_lines_past_last_line() {
799        let mut backend = TestBackend::with_lines([
800            "aaaaaaaaaa",
801            "bbbbbbbbbb",
802            "cccccccccc",
803            "dddddddddd",
804            "eeeeeeeeee",
805        ]);
806
807        backend
808            .set_cursor_position(Position { x: 0, y: 3 })
809            .unwrap();
810
811        backend.append_lines(3).unwrap();
812        backend.assert_cursor_position(Position { x: 1, y: 4 });
813
814        backend.assert_buffer_lines([
815            "cccccccccc",
816            "dddddddddd",
817            "eeeeeeeeee",
818            "          ",
819            "          ",
820        ]);
821        backend.assert_scrollback_lines(["aaaaaaaaaa", "bbbbbbbbbb"]);
822    }
823
824    #[test]
825    fn append_multiple_lines_where_cursor_at_end_appends_height_lines() {
826        let mut backend = TestBackend::with_lines([
827            "aaaaaaaaaa",
828            "bbbbbbbbbb",
829            "cccccccccc",
830            "dddddddddd",
831            "eeeeeeeeee",
832        ]);
833
834        backend
835            .set_cursor_position(Position { x: 0, y: 4 })
836            .unwrap();
837
838        backend.append_lines(5).unwrap();
839        backend.assert_cursor_position(Position { x: 1, y: 4 });
840
841        backend.assert_buffer_lines([
842            "          ",
843            "          ",
844            "          ",
845            "          ",
846            "          ",
847        ]);
848        backend.assert_scrollback_lines([
849            "aaaaaaaaaa",
850            "bbbbbbbbbb",
851            "cccccccccc",
852            "dddddddddd",
853            "eeeeeeeeee",
854        ]);
855    }
856
857    #[test]
858    fn append_multiple_lines_where_cursor_appends_height_lines() {
859        let mut backend = TestBackend::with_lines([
860            "aaaaaaaaaa",
861            "bbbbbbbbbb",
862            "cccccccccc",
863            "dddddddddd",
864            "eeeeeeeeee",
865        ]);
866
867        backend.set_cursor_position(Position::ORIGIN).unwrap();
868
869        backend.append_lines(5).unwrap();
870        backend.assert_cursor_position(Position { x: 1, y: 4 });
871
872        backend.assert_buffer_lines([
873            "bbbbbbbbbb",
874            "cccccccccc",
875            "dddddddddd",
876            "eeeeeeeeee",
877            "          ",
878        ]);
879        backend.assert_scrollback_lines(["aaaaaaaaaa"]);
880    }
881
882    #[test]
883    fn append_multiple_lines_where_cursor_at_end_appends_more_than_height_lines() {
884        let mut backend = TestBackend::with_lines([
885            "aaaaaaaaaa",
886            "bbbbbbbbbb",
887            "cccccccccc",
888            "dddddddddd",
889            "eeeeeeeeee",
890        ]);
891
892        backend
893            .set_cursor_position(Position { x: 0, y: 4 })
894            .unwrap();
895
896        backend.append_lines(8).unwrap();
897        backend.assert_cursor_position(Position { x: 1, y: 4 });
898
899        backend.assert_buffer_lines([
900            "          ",
901            "          ",
902            "          ",
903            "          ",
904            "          ",
905        ]);
906        backend.assert_scrollback_lines([
907            "aaaaaaaaaa",
908            "bbbbbbbbbb",
909            "cccccccccc",
910            "dddddddddd",
911            "eeeeeeeeee",
912            "          ",
913            "          ",
914            "          ",
915        ]);
916    }
917
918    #[test]
919    fn append_lines_truncates_beyond_u16_max() -> io::Result<()> {
920        let mut backend = TestBackend::new(10, 5);
921
922        // Fill the scrollback with 65535 + 10 lines.
923        let row_count = u16::MAX as usize + 10;
924        for row in 0..=row_count {
925            if row > 4 {
926                backend.set_cursor_position(Position { x: 0, y: 4 })?;
927                backend.append_lines(1)?;
928            }
929            let cells = format!("{row:>10}").chars().map(Cell::from).collect_vec();
930            let content = cells
931                .iter()
932                .enumerate()
933                .map(|(column, cell)| (column as u16, 4.min(row) as u16, cell));
934            backend.draw(content)?;
935        }
936
937        // check that the buffer contains the last 5 lines appended
938        backend.assert_buffer_lines([
939            "     65541",
940            "     65542",
941            "     65543",
942            "     65544",
943            "     65545",
944        ]);
945
946        // TODO: ideally this should be something like:
947        //     let lines = (6..=65545).map(|row| format!("{row:>10}"));
948        //     backend.assert_scrollback_lines(lines);
949        // but there's some truncation happening in Buffer::with_lines that needs to be fixed
950        assert_eq!(
951            Buffer {
952                area: Rect::new(0, 0, 10, 5),
953                content: backend.scrollback.content[0..10 * 5].to_vec(),
954            },
955            Buffer::with_lines([
956                "         6",
957                "         7",
958                "         8",
959                "         9",
960                "        10",
961            ]),
962            "first 5 lines of scrollback should have been truncated"
963        );
964
965        assert_eq!(
966            Buffer {
967                area: Rect::new(0, 0, 10, 5),
968                content: backend.scrollback.content[10 * 65530..10 * 65535].to_vec(),
969            },
970            Buffer::with_lines([
971                "     65536",
972                "     65537",
973                "     65538",
974                "     65539",
975                "     65540",
976            ]),
977            "last 5 lines of scrollback should have been appended"
978        );
979
980        // These checks come after the content checks as otherwise we won't see the failing content
981        // when these checks fail.
982        // Make sure the scrollback is the right size.
983        assert_eq!(backend.scrollback.area.width, 10);
984        assert_eq!(backend.scrollback.area.height, 65535);
985        assert_eq!(backend.scrollback.content.len(), 10 * 65535);
986        Ok(())
987    }
988
989    #[test]
990    fn size() {
991        let backend = TestBackend::new(10, 2);
992        assert_eq!(backend.size().unwrap(), Size::new(10, 2));
993    }
994
995    #[test]
996    fn flush() {
997        let mut backend = TestBackend::new(10, 2);
998        backend.flush().unwrap();
999    }
1000
1001    #[cfg(feature = "scrolling-regions")]
1002    mod scrolling_regions {
1003        use rstest::rstest;
1004
1005        use super::*;
1006
1007        const A: &str = "aaaa";
1008        const B: &str = "bbbb";
1009        const C: &str = "cccc";
1010        const D: &str = "dddd";
1011        const E: &str = "eeee";
1012        const S: &str = "    ";
1013
1014        #[rstest]
1015        #[case([A, B, C, D, E], 0..5, 0, [],                    [A, B, C, D, E])]
1016        #[case([A, B, C, D, E], 0..5, 2, [A, B],                [C, D, E, S, S])]
1017        #[case([A, B, C, D, E], 0..5, 5, [A, B, C, D, E],       [S, S, S, S, S])]
1018        #[case([A, B, C, D, E], 0..5, 7, [A, B, C, D, E, S, S], [S, S, S, S, S])]
1019        #[case([A, B, C, D, E], 0..3, 0, [],                    [A, B, C, D, E])]
1020        #[case([A, B, C, D, E], 0..3, 2, [A, B],                [C, S, S, D, E])]
1021        #[case([A, B, C, D, E], 0..3, 3, [A, B, C],             [S, S, S, D, E])]
1022        #[case([A, B, C, D, E], 0..3, 4, [A, B, C, S],          [S, S, S, D, E])]
1023        #[case([A, B, C, D, E], 1..4, 0, [],                    [A, B, C, D, E])]
1024        #[case([A, B, C, D, E], 1..4, 2, [],                    [A, D, S, S, E])]
1025        #[case([A, B, C, D, E], 1..4, 3, [],                    [A, S, S, S, E])]
1026        #[case([A, B, C, D, E], 1..4, 4, [],                    [A, S, S, S, E])]
1027        #[case([A, B, C, D, E], 0..0, 0, [],                    [A, B, C, D, E])]
1028        #[case([A, B, C, D, E], 0..0, 2, [S, S],                [A, B, C, D, E])]
1029        #[case([A, B, C, D, E], 2..2, 0, [],                    [A, B, C, D, E])]
1030        #[case([A, B, C, D, E], 2..2, 2, [],                    [A, B, C, D, E])]
1031        fn scroll_region_up<const L: usize, const M: usize, const N: usize>(
1032            #[case] initial_screen: [&'static str; L],
1033            #[case] range: std::ops::Range<u16>,
1034            #[case] scroll_by: u16,
1035            #[case] expected_scrollback: [&'static str; M],
1036            #[case] expected_buffer: [&'static str; N],
1037        ) {
1038            let mut backend = TestBackend::with_lines(initial_screen);
1039            backend.scroll_region_up(range, scroll_by).unwrap();
1040            if expected_scrollback.is_empty() {
1041                backend.assert_scrollback_empty();
1042            } else {
1043                backend.assert_scrollback_lines(expected_scrollback);
1044            }
1045            backend.assert_buffer_lines(expected_buffer);
1046        }
1047
1048        #[rstest]
1049        #[case([A, B, C, D, E], 0..5, 0, [A, B, C, D, E])]
1050        #[case([A, B, C, D, E], 0..5, 2, [S, S, A, B, C])]
1051        #[case([A, B, C, D, E], 0..5, 5, [S, S, S, S, S])]
1052        #[case([A, B, C, D, E], 0..5, 7, [S, S, S, S, S])]
1053        #[case([A, B, C, D, E], 0..3, 0, [A, B, C, D, E])]
1054        #[case([A, B, C, D, E], 0..3, 2, [S, S, A, D, E])]
1055        #[case([A, B, C, D, E], 0..3, 3, [S, S, S, D, E])]
1056        #[case([A, B, C, D, E], 0..3, 4, [S, S, S, D, E])]
1057        #[case([A, B, C, D, E], 1..4, 0, [A, B, C, D, E])]
1058        #[case([A, B, C, D, E], 1..4, 2, [A, S, S, B, E])]
1059        #[case([A, B, C, D, E], 1..4, 3, [A, S, S, S, E])]
1060        #[case([A, B, C, D, E], 1..4, 4, [A, S, S, S, E])]
1061        #[case([A, B, C, D, E], 0..0, 0, [A, B, C, D, E])]
1062        #[case([A, B, C, D, E], 0..0, 2, [A, B, C, D, E])]
1063        #[case([A, B, C, D, E], 2..2, 0, [A, B, C, D, E])]
1064        #[case([A, B, C, D, E], 2..2, 2, [A, B, C, D, E])]
1065        fn scroll_region_down<const M: usize, const N: usize>(
1066            #[case] initial_screen: [&'static str; M],
1067            #[case] range: std::ops::Range<u16>,
1068            #[case] scroll_by: u16,
1069            #[case] expected_buffer: [&'static str; N],
1070        ) {
1071            let mut backend = TestBackend::with_lines(initial_screen);
1072            backend.scroll_region_down(range, scroll_by).unwrap();
1073            backend.assert_scrollback_empty();
1074            backend.assert_buffer_lines(expected_buffer);
1075        }
1076    }
1077}