Skip to main content

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