//! Tests for a terminal whose width disagrees with the round's: a //! terminal resized while a round streams, and a table too wide for any //! terminal. These drive a `Transcript` directly, because the harness in //! `screen_tests.rs` plays keys and turn events, and a width change is //! neither. use ratatui::backend::TestBackend; use ratatui::layout::Position; use ratatui::{TerminalOptions, Viewport}; use crate::play::screen::tests::buffer_text; use super::*; /// A paragraph that wraps to four rows at [`NARROW`] and to one at /// [`WIDE`]. const PARAGRAPH: &str = "one two three four five six seven eight nine ten eleven twelve"; /// The block after [`PARAGRAPH`], which finishes the round. const AFTER: &str = "thirteen fourteen"; const NARROW: u16 = 20; const WIDE: u16 = 80; /// A tail area with room for every row these rounds form, so nothing /// spills and the width is the only thing under test. const DEEP_TAIL: u16 = 10; /// How many rows the test terminals hold. The viewport fills the screen, /// so every row the transcript emits lands in the scrollback. const HEIGHT: u16 = 4; /// A terminal `width` columns wide. /// /// A round gathers its rows against one of these and places them on it, /// so the rows of a round that spans a resize land on the terminal that /// was there when they were gathered. /// /// A resize is a second terminal here, not `TestBackend::resize`. That /// call re-slices the scrollback to the new width, which garbles every /// row already in it and leaves nothing to assert against. The width the /// terminal reports is the only thing a resize changes for a /// `Transcript`, and each terminal keeps a scrollback of its own, so the /// rows that went out before the change stay legible next to the ones /// that went out after. fn terminal(width: u16) -> Terminal { let mut backend = TestBackend::new(width, HEIGHT); backend.set_cursor_position(Position::new(0, 0)).unwrap(); Terminal::with_options( backend, TerminalOptions { viewport: Viewport::Inline(HEIGHT), }, ) .unwrap() } /// The rows of `terminal`'s scrollback. fn scrollback(terminal: &Terminal) -> Vec { buffer_text(terminal.backend().scrollback()) .lines() .map(str::to_string) .collect() } #[test] fn a_round_that_widens_mid_stream_still_says_what_it_has_left() { let mut narrow = terminal(NARROW); let mut wide = terminal(WIDE); let mut transcript = Transcript::default(); transcript.delta(&format!("{PARAGRAPH}\n\n{AFTER}")); transcript.stream(&narrow, DEEP_TAIL).unwrap(); transcript.place(&mut narrow).unwrap(); transcript.delta(" fifteen"); transcript.stream(&wide, DEEP_TAIL).unwrap(); transcript.flush(&wide).unwrap(); transcript.place(&mut wide).unwrap(); assert_eq!( scrollback(&narrow), [ "one two three four", "five six seven eight", "nine ten eleven", "twelve", ] ); assert_eq!(scrollback(&wide), ["", "thirteen fourteen", "fifteen"]); } #[test] fn a_round_takes_the_width_it_first_has_something_to_say_at() { let wide = terminal(WIDE); let mut narrow = terminal(NARROW); let mut transcript = Transcript::default(); // The play loop streams several times a second, whether or not the // DM has said anything yet. transcript.stream(&wide, DEEP_TAIL).unwrap(); transcript.delta(PARAGRAPH); transcript.stream(&narrow, 1).unwrap(); transcript.place(&mut narrow).unwrap(); // The paragraph takes four rows at twenty columns, and a tail area // of one row spills the first three of them. A round holding the // eighty columns of the idle pass would render one row and spill // none of it. assert_eq!( scrollback(&narrow), [ "one two three four", "five six seven eight", "nine ten eleven", ] ); } #[test] fn a_round_that_narrows_mid_stream_says_nothing_a_second_time() { let mut wide = terminal(WIDE); let mut narrow = terminal(NARROW); let mut transcript = Transcript::default(); transcript.delta(&format!("{PARAGRAPH}\n\n{AFTER}")); transcript.stream(&wide, DEEP_TAIL).unwrap(); transcript.place(&mut wide).unwrap(); transcript.flush(&narrow).unwrap(); transcript.place(&mut narrow).unwrap(); assert_eq!(scrollback(&wide), [PARAGRAPH]); assert_eq!(scrollback(&narrow), ["", AFTER]); } #[test] fn a_row_too_wide_for_the_terminal_it_lands_on_keeps_every_word() { let mut wide = terminal(WIDE); let mut narrow = terminal(NARROW); let mut transcript = Transcript::default(); transcript.delta(&format!("{AFTER}\n\n{PARAGRAPH}")); transcript.stream(&wide, DEEP_TAIL).unwrap(); transcript.place(&mut wide).unwrap(); transcript.flush(&narrow).unwrap(); transcript.place(&mut narrow).unwrap(); // The paragraph rendered as one row at eighty columns. All of it is // here, over the four rows twenty columns takes, and the row that // separates it from the block above is one blank row of spacing. assert_eq!(scrollback(&wide), [AFTER]); assert_eq!( scrollback(&narrow), [ "", "one two three four", "five six seven eight", "nine ten eleven", "twelve", ] ); } /// A paragraph of two-column characters. It prints in twenty-three /// columns, which [`WIDE`] holds and [`NARROW`] does not, and in fourteen /// characters, which both of them hold: a wrap that counts characters /// leaves it too wide for [`NARROW`]. const WIDE_CHARACTERS: &str = "日本語 の 文字 は 二 列"; /// The rows waiting to go out, as the text they carry. fn queued_rows(transcript: &Transcript) -> Vec { transcript .queued .iter() .map(|queued| queued.row.to_string()) .collect() } /// A transcript holding one round of [`WIDE_CHARACTERS`], rendered at /// [`WIDE`] and then flushed onto a terminal [`NARROW`] columns wide, so /// its one row has to wrap again to reach the screen whole. fn wide_round_flushed_narrow() -> Transcript { let wide = terminal(WIDE); let narrow = terminal(NARROW); let mut transcript = Transcript::default(); transcript.delta(WIDE_CHARACTERS); transcript.stream(&wide, DEEP_TAIL).unwrap(); transcript.flush(&narrow).unwrap(); transcript } #[test] fn a_row_of_wide_characters_wraps_to_the_columns_the_terminal_has() { let transcript = wide_round_flushed_narrow(); assert_eq!(queued_rows(&transcript), ["日本語 の 文字 は 二", "列"]); } #[test] fn every_row_of_a_wrapped_wide_row_fits_the_terminal() { let transcript = wide_round_flushed_narrow(); let widest = transcript .queued .iter() .map(|queued| queued.row.width()) .max(); assert_eq!(widest, Some(NARROW as usize)); } #[test] fn a_row_of_zero_width_characters_still_goes_out() { let wide = terminal(WIDE); let mut transcript = Transcript::default(); transcript .insert(&wide, "\u{200b}", aside(), Kind::Aside) .unwrap(); assert_eq!(queued_rows(&transcript), ["\u{200b}"]); } /// A table of four columns, each already at the narrowest a column /// shrinks to. It renders twenty-five columns wide at any width, so /// [`NARROW`] cannot hold it. const WIDE_TABLE: &str = "| abc | def | ghi | jkl |\n|-----|-----|-----|-----|\n| mno | pqr | stu | vwx |\n"; #[test] fn a_table_too_wide_for_the_screen_clips_every_row_at_the_same_column() { let mut narrow = terminal(NARROW); let mut transcript = Transcript::default(); transcript.delta(WIDE_TABLE); transcript.flush(&narrow).unwrap(); transcript.place(&mut narrow).unwrap(); // One scrollback row per table row, every one of them cut at column // twenty, so the three columns that fit still line up. The content // rows show nineteen characters because the twentieth is the space // that opens the fourth column, and `scrollback` trims a trailing // space. assert_eq!( scrollback(&narrow), [ "┌─────┬─────┬─────┬─", "│ abc │ def │ ghi │", "├─────┼─────┼─────┼─", "│ mno │ pqr │ stu │", "└─────┴─────┴─────┴─", ] ); }