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 unicode_width::UnicodeWidthStr;
10
11use crate::backend::{Backend, ClearType, WindowSize};
12use crate::buffer::{Buffer, Cell};
13use crate::layout::{Position, Rect, Size};
14
15/// A [`Backend`] implementation used for integration testing that renders to an memory buffer.
16///
17/// Note: that although many of the integration and unit tests in ratatui are written using this
18/// backend, it is preferable to write unit tests for widgets directly against the buffer rather
19/// than using this backend. This backend is intended for integration tests that test the entire
20/// terminal UI.
21///
22/// # Example
23///
24/// ```rust,ignore
25/// use ratatui::backend::{Backend, TestBackend};
26///
27/// let mut backend = TestBackend::new(10, 2);
28/// backend.clear()?;
29/// backend.assert_buffer_lines(["          "; 2]);
30/// # Result::Ok(())
31/// ```
32#[derive(Debug, Clone, Eq, PartialEq, Hash)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub struct TestBackend {
35    buffer: Buffer,
36    scrollback: Buffer,
37    cursor: bool,
38    pos: (u16, u16),
39}
40
41/// Returns a string representation of the given buffer for debugging purpose.
42///
43/// This function is used to visualize the buffer content in a human-readable format.
44/// It iterates through the buffer content and appends each cell's symbol to the view string.
45/// If a cell is hidden by a multi-width symbol, it is added to the overwritten vector and
46/// displayed at the end of the line.
47fn buffer_view(buffer: &Buffer) -> String {
48    let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3);
49    for cells in buffer.content.chunks(buffer.area.width as usize) {
50        let mut overwritten = vec![];
51        let mut skip: usize = 0;
52        view.push('"');
53        for (x, c) in cells.iter().enumerate() {
54            if skip == 0 {
55                view.push_str(c.symbol());
56            } else {
57                overwritten.push((x, c.symbol()));
58            }
59            skip = core::cmp::max(skip, c.symbol().width()).saturating_sub(1);
60        }
61        view.push('"');
62        if !overwritten.is_empty() {
63            write!(&mut view, " Hidden by multi-width symbols: {overwritten:?}").unwrap();
64        }
65        view.push('\n');
66    }
67    view
68}
69
70impl TestBackend {
71    /// Creates a new `TestBackend` with the specified width and height.
72    pub fn new(width: u16, height: u16) -> Self {
73        Self {
74            buffer: Buffer::empty(Rect::new(0, 0, width, height)),
75            scrollback: Buffer::empty(Rect::new(0, 0, width, 0)),
76            cursor: false,
77            pos: (0, 0),
78        }
79    }
80
81    /// Creates a new `TestBackend` with the specified lines as the initial screen state.
82    ///
83    /// The backend's screen size is determined from the initial lines.
84    #[must_use]
85    pub fn with_lines<'line, Lines>(lines: Lines) -> Self
86    where
87        Lines: IntoIterator,
88        Lines::Item: Into<crate::text::Line<'line>>,
89    {
90        let buffer = Buffer::with_lines(lines);
91        let scrollback = Buffer::empty(Rect {
92            width: buffer.area.width,
93            ..Rect::ZERO
94        });
95        Self {
96            buffer,
97            scrollback,
98            cursor: false,
99            pos: (0, 0),
100        }
101    }
102
103    /// Returns a reference to the internal buffer of the `TestBackend`.
104    pub const fn buffer(&self) -> &Buffer {
105        &self.buffer
106    }
107
108    /// Returns a reference to the internal scrollback buffer of the `TestBackend`.
109    ///
110    /// The scrollback buffer represents the part of the screen that is currently hidden from view,
111    /// but that could be accessed by scrolling back in the terminal's history. This would normally
112    /// be done using the terminal's scrollbar or an equivalent keyboard shortcut.
113    ///
114    /// The scrollback buffer starts out empty. Lines are appended when they scroll off the top of
115    /// the main buffer. This happens when lines are appended to the bottom of the main buffer
116    /// using [`Backend::append_lines`].
117    ///
118    /// The scrollback buffer has a maximum height of [`u16::MAX`]. If lines are appended to the
119    /// bottom of the scrollback buffer when it is at its maximum height, a corresponding number of
120    /// lines will be removed from the top.
121    pub const fn scrollback(&self) -> &Buffer {
122        &self.scrollback
123    }
124
125    /// Resizes the `TestBackend` to the specified width and height.
126    pub fn resize(&mut self, width: u16, height: u16) {
127        self.buffer.resize(Rect::new(0, 0, width, height));
128        let scrollback_height = self.scrollback.area.height;
129        self.scrollback
130            .resize(Rect::new(0, 0, width, scrollback_height));
131    }
132
133    /// Asserts that the `TestBackend`'s buffer is equal to the expected buffer.
134    ///
135    /// This is a shortcut for `assert_eq!(self.buffer(), &expected)`.
136    ///
137    /// # Panics
138    ///
139    /// When they are not equal, a panic occurs with a detailed error message showing the
140    /// differences between the expected and actual buffers.
141    #[expect(deprecated)]
142    #[track_caller]
143    pub fn assert_buffer(&self, expected: &Buffer) {
144        // TODO: use assert_eq!()
145        crate::assert_buffer_eq!(&self.buffer, expected);
146    }
147
148    /// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected buffer.
149    ///
150    /// This is a shortcut for `assert_eq!(self.scrollback(), &expected)`.
151    ///
152    /// # Panics
153    ///
154    /// When they are not equal, a panic occurs with a detailed error message showing the
155    /// differences between the expected and actual buffers.
156    #[track_caller]
157    pub fn assert_scrollback(&self, expected: &Buffer) {
158        assert_eq!(&self.scrollback, expected);
159    }
160
161    /// Asserts that the `TestBackend`'s scrollback buffer is empty.
162    ///
163    /// # Panics
164    ///
165    /// When the scrollback buffer is not equal, a panic occurs with a detailed error message
166    /// showing the differences between the expected and actual buffers.
167    pub fn assert_scrollback_empty(&self) {
168        let expected = Buffer {
169            area: Rect {
170                width: self.scrollback.area.width,
171                ..Rect::ZERO
172            },
173            content: vec![],
174        };
175        self.assert_scrollback(&expected);
176    }
177
178    /// Asserts that the `TestBackend`'s buffer is equal to the expected lines.
179    ///
180    /// This is a shortcut for `assert_eq!(self.buffer(), &Buffer::with_lines(expected))`.
181    ///
182    /// # Panics
183    ///
184    /// When they are not equal, a panic occurs with a detailed error message showing the
185    /// differences between the expected and actual buffers.
186    #[track_caller]
187    pub fn assert_buffer_lines<'line, Lines>(&self, expected: Lines)
188    where
189        Lines: IntoIterator,
190        Lines::Item: Into<crate::text::Line<'line>>,
191    {
192        self.assert_buffer(&Buffer::with_lines(expected));
193    }
194
195    /// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected lines.
196    ///
197    /// This is a shortcut for `assert_eq!(self.scrollback(), &Buffer::with_lines(expected))`.
198    ///
199    /// # Panics
200    ///
201    /// When they are not equal, a panic occurs with a detailed error message showing the
202    /// differences between the expected and actual buffers.
203    #[track_caller]
204    pub fn assert_scrollback_lines<'line, Lines>(&self, expected: Lines)
205    where
206        Lines: IntoIterator,
207        Lines::Item: Into<crate::text::Line<'line>>,
208    {
209        self.assert_scrollback(&Buffer::with_lines(expected));
210    }
211
212    /// Asserts that the `TestBackend`'s cursor position is equal to the expected one.
213    ///
214    /// This is a shortcut for `assert_eq!(self.get_cursor_position().unwrap(), expected)`.
215    ///
216    /// # Panics
217    ///
218    /// When they are not equal, a panic occurs with a detailed error message showing the
219    /// differences between the expected and actual position.
220    #[track_caller]
221    pub fn assert_cursor_position<P: Into<Position>>(&mut self, position: P) {
222        let actual = self.get_cursor_position().unwrap();
223        assert_eq!(actual, position.into());
224    }
225}
226
227impl fmt::Display for TestBackend {
228    /// Formats the `TestBackend` for display by calling the `buffer_view` function
229    /// on its internal buffer.
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        write!(f, "{}", buffer_view(&self.buffer))
232    }
233}
234
235type Result<T, E = core::convert::Infallible> = core::result::Result<T, E>;
236
237impl Backend for TestBackend {
238    type Error = core::convert::Infallible;
239
240    fn draw<'a, I>(&mut self, content: I) -> Result<()>
241    where
242        I: Iterator<Item = (u16, u16, &'a Cell)>,
243    {
244        for (x, y, c) in content {
245            self.buffer[(x, y)] = c.clone();
246        }
247        Ok(())
248    }
249
250    fn hide_cursor(&mut self) -> Result<()> {
251        self.cursor = false;
252        Ok(())
253    }
254
255    fn show_cursor(&mut self) -> Result<()> {
256        self.cursor = true;
257        Ok(())
258    }
259
260    fn get_cursor_position(&mut self) -> Result<Position> {
261        Ok(self.pos.into())
262    }
263
264    fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<()> {
265        self.pos = position.into().into();
266        Ok(())
267    }
268
269    fn clear(&mut self) -> Result<()> {
270        self.buffer.reset();
271        Ok(())
272    }
273
274    fn clear_region(&mut self, clear_type: ClearType) -> Result<()> {
275        let region = match clear_type {
276            ClearType::All => return self.clear(),
277            ClearType::AfterCursor => {
278                let index = self.buffer.index_of(self.pos.0, self.pos.1) + 1;
279                &mut self.buffer.content[index..]
280            }
281            ClearType::BeforeCursor => {
282                let index = self.buffer.index_of(self.pos.0, self.pos.1);
283                &mut self.buffer.content[..index]
284            }
285            ClearType::CurrentLine => {
286                let line_start_index = self.buffer.index_of(0, self.pos.1);
287                let line_end_index = self.buffer.index_of(self.buffer.area.width - 1, self.pos.1);
288                &mut self.buffer.content[line_start_index..=line_end_index]
289            }
290            ClearType::UntilNewLine => {
291                let index = self.buffer.index_of(self.pos.0, self.pos.1);
292                let line_end_index = self.buffer.index_of(self.buffer.area.width - 1, self.pos.1);
293                &mut self.buffer.content[index..=line_end_index]
294            }
295        };
296        for cell in region {
297            cell.reset();
298        }
299        Ok(())
300    }
301
302    /// Inserts n line breaks at the current cursor position.
303    ///
304    /// After the insertion, the cursor x position will be incremented by 1 (unless it's already
305    /// at the end of line). This is a common behaviour of terminals in raw mode.
306    ///
307    /// If the number of lines to append is fewer than the number of lines in the buffer after the
308    /// cursor y position then the cursor is moved down by n rows.
309    ///
310    /// If the number of lines to append is greater than the number of lines in the buffer after
311    /// the cursor y position then that number of empty lines (at most the buffer's height in this
312    /// case but this limit is instead replaced with scrolling in most backend implementations) will
313    /// be added after the current position and the cursor will be moved to the last row.
314    fn append_lines(&mut self, line_count: u16) -> Result<()> {
315        let Position { x: cur_x, y: cur_y } = self.get_cursor_position()?;
316        let Rect { width, height, .. } = self.buffer.area;
317
318        // the next column ensuring that we don't go past the last column
319        let new_cursor_x = cur_x.saturating_add(1).min(width.saturating_sub(1));
320
321        let max_y = height.saturating_sub(1);
322        let lines_after_cursor = max_y.saturating_sub(cur_y);
323
324        if line_count > lines_after_cursor {
325            // We need to insert blank lines at the bottom and scroll the lines from the top into
326            // scrollback.
327            let scroll_by: usize = (line_count - lines_after_cursor).into();
328            let width: usize = self.buffer.area.width.into();
329            let cells_to_scrollback = self.buffer.content.len().min(width * scroll_by);
330
331            append_to_scrollback(
332                &mut self.scrollback,
333                self.buffer.content.splice(
334                    0..cells_to_scrollback,
335                    iter::repeat_with(Default::default).take(cells_to_scrollback),
336                ),
337            );
338            self.buffer.content.rotate_left(cells_to_scrollback);
339            append_to_scrollback(
340                &mut self.scrollback,
341                iter::repeat_with(Default::default).take(width * scroll_by - cells_to_scrollback),
342            );
343        }
344
345        let new_cursor_y = cur_y.saturating_add(line_count).min(max_y);
346        self.set_cursor_position(Position::new(new_cursor_x, new_cursor_y))?;
347
348        Ok(())
349    }
350
351    fn size(&self) -> Result<Size> {
352        Ok(self.buffer.area.as_size())
353    }
354
355    fn window_size(&mut self) -> Result<WindowSize> {
356        // Some arbitrary window pixel size, probably doesn't need much testing.
357        const WINDOW_PIXEL_SIZE: Size = Size {
358            width: 640,
359            height: 480,
360        };
361        Ok(WindowSize {
362            columns_rows: self.buffer.area.as_size(),
363            pixels: WINDOW_PIXEL_SIZE,
364        })
365    }
366
367    fn flush(&mut self) -> Result<()> {
368        Ok(())
369    }
370
371    #[cfg(feature = "scrolling-regions")]
372    fn scroll_region_up(&mut self, region: core::ops::Range<u16>, scroll_by: u16) -> Result<()> {
373        let width: usize = self.buffer.area.width.into();
374        let cell_region_start = width * region.start.min(self.buffer.area.height) as usize;
375        let cell_region_end = width * region.end.min(self.buffer.area.height) as usize;
376        let cell_region_len = cell_region_end - cell_region_start;
377        let cells_to_scroll_by = width * scroll_by as usize;
378
379        // Deal with the simple case where nothing needs to be copied into scrollback.
380        if cell_region_start > 0 {
381            if cells_to_scroll_by >= cell_region_len {
382                // The scroll amount is large enough to clear the whole region.
383                self.buffer.content[cell_region_start..cell_region_end].fill_with(Default::default);
384            } else {
385                // Scroll up by rotating, then filling in the bottom with empty cells.
386                self.buffer.content[cell_region_start..cell_region_end]
387                    .rotate_left(cells_to_scroll_by);
388                self.buffer.content[cell_region_end - cells_to_scroll_by..cell_region_end]
389                    .fill_with(Default::default);
390            }
391            return Ok(());
392        }
393
394        // The rows inserted into the scrollback will first come from the buffer, and if that is
395        // insufficient, will then be blank rows.
396        let cells_from_region = cell_region_len.min(cells_to_scroll_by);
397        append_to_scrollback(
398            &mut self.scrollback,
399            self.buffer.content.splice(
400                0..cells_from_region,
401                iter::repeat_with(Default::default).take(cells_from_region),
402            ),
403        );
404        if cells_to_scroll_by < cell_region_len {
405            // Rotate the remaining cells to the front of the region.
406            self.buffer.content[cell_region_start..cell_region_end].rotate_left(cells_from_region);
407        } else {
408            // Splice cleared out the region. Insert empty rows in scrollback.
409            append_to_scrollback(
410                &mut self.scrollback,
411                iter::repeat_with(Default::default).take(cells_to_scroll_by - cell_region_len),
412            );
413        }
414        Ok(())
415    }
416
417    #[cfg(feature = "scrolling-regions")]
418    fn scroll_region_down(&mut self, region: core::ops::Range<u16>, scroll_by: u16) -> Result<()> {
419        let width: usize = self.buffer.area.width.into();
420        let cell_region_start = width * region.start.min(self.buffer.area.height) as usize;
421        let cell_region_end = width * region.end.min(self.buffer.area.height) as usize;
422        let cell_region_len = cell_region_end - cell_region_start;
423        let cells_to_scroll_by = width * scroll_by as usize;
424
425        if cells_to_scroll_by >= cell_region_len {
426            // The scroll amount is large enough to clear the whole region.
427            self.buffer.content[cell_region_start..cell_region_end].fill_with(Default::default);
428        } else {
429            // Scroll up by rotating, then filling in the top with empty cells.
430            self.buffer.content[cell_region_start..cell_region_end]
431                .rotate_right(cells_to_scroll_by);
432            self.buffer.content[cell_region_start..cell_region_start + cells_to_scroll_by]
433                .fill_with(Default::default);
434        }
435        Ok(())
436    }
437}
438
439/// Append the provided cells to the bottom of a scrollback buffer. The number of cells must be a
440/// multiple of the buffer's width. If the scrollback buffer ends up larger than 65535 lines tall,
441/// then lines will be removed from the top to get it down to size.
442fn append_to_scrollback(scrollback: &mut Buffer, cells: impl IntoIterator<Item = Cell>) {
443    scrollback.content.extend(cells);
444    let width = scrollback.area.width as usize;
445    let new_height = (scrollback.content.len() / width).min(u16::MAX as usize);
446    let keep_from = scrollback
447        .content
448        .len()
449        .saturating_sub(width * u16::MAX as usize);
450    scrollback.content.drain(0..keep_from);
451    scrollback.area.height = new_height as u16;
452}
453
454#[cfg(test)]
455mod tests {
456    use alloc::format;
457
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() -> 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: core::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: core::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}