//! Tests for [`super`]. #[test] fn one_line_takes_the_error_then_the_note_then_the_hint() { // A terminal field has room for exactly one message, so the three // channels compete and `Field::note` decides the order. let style = PieceStyle::default(); let mut f = Field::new(FieldKind::Text, "title", "Title"); f.hint = Some("how it works"); assert_eq!(message_of(&style, &f).unwrap().0, "how it works"); f.note = Some((Tone::Warning, "what it costs")); assert_eq!(message_of(&style, &f).unwrap().0, "what it costs"); assert_eq!(message_of(&style, &f).unwrap().1, style.warning); f.error = Some("what is wrong"); assert_eq!(message_of(&style, &f).unwrap().0, "what is wrong"); assert_eq!(message_of(&style, &f).unwrap().1, style.danger); // A note carries its own tone, so a quiet one is not painted as a // warning just for being a note. f.error = None; f.note = Some((Tone::Neutral, "an ordinary fact")); assert_eq!(message_of(&style, &f).unwrap().1, style.content); } use super::*; use makeover_layout::{Choice, State}; /// The style the drawings are read against: one distinguishable modifier /// per role, so a test can say which style landed without a colour. fn style() -> PieceStyle { PieceStyle { content: Style::new().add_modifier(Modifier::BOLD), secondary: Style::new().add_modifier(Modifier::ITALIC), muted: Style::new().add_modifier(Modifier::DIM), danger: Style::new().add_modifier(Modifier::CROSSED_OUT), ..PieceStyle::default() } } fn buffer(width: u16, height: u16) -> Buffer { Buffer::empty(Rect::new(0, 0, width, height)) } /// Everything in the buffer, one string per row. fn rows(buf: &Buffer) -> Vec { (0..buf.area.height) .map(|y| { (0..buf.area.width) .map(|x| { buf.cell((x, y)) .map_or(' ', |c| c.symbol().chars().next().unwrap_or(' ')) }) .collect::() .trim_end() .to_owned() }) .collect() } #[test] fn a_bar_fills_in_proportion_and_reads_out_the_two_numbers() { let style = style(); let line = meter(&style, &Meter::new(3, 10).label("subtasks")); let drawn: String = line.spans.iter().map(|s| s.content.as_ref()).collect(); assert_eq!(drawn, "###------- 3/10 subtasks"); // The noun is optional and the ratio is not, because a bar with no // reading is a bar you cannot check. let bare = meter(&style, &Meter::new(3, 10)); let drawn: String = bare.spans.iter().map(|s| s.content.as_ref()).collect(); assert_eq!(drawn, "###------- 3/10"); } #[test] fn an_empty_set_is_an_empty_bar_rather_than_a_divide_by_zero() { // `Meter::total` of zero means there is no set, and the checked // division is what keeps that from being a panic in a draw. let line = meter(&style(), &Meter::new(0, 0)); let drawn: String = line.spans.iter().map(|s| s.content.as_ref()).collect(); assert_eq!(drawn, "---------- 0/0"); } #[test] fn an_over_run_fills_the_bar_and_still_reports_the_overflow() { // The clamp is for drawing only. The reading is what keeps the fact // `Meter::percent` destroys. let line = meter(&style(), &Meter::new(14, 10)); let drawn: String = line.spans.iter().map(|s| s.content.as_ref()).collect(); assert_eq!(drawn, "########## 14/10"); } #[test] fn a_badge_is_round_and_a_chip_is_square() { // The one affordance a cell has left once colour is spent on the tone, // and the whole of how a terminal says "this one answers a press". let style = style(); let badge = token(&style, "draft", Token::Badge, Tone::Neutral, false, false); assert_eq!(badge.content.as_ref(), "(draft)"); let chip = token( &style, "rust", Token::Chip { removable: false }, Tone::Neutral, false, false, ); assert_eq!(chip.content.as_ref(), "[rust]"); } #[test] fn a_latched_chip_reads_the_same_as_a_focused_one() { // The collision a terminal cannot avoid, asserted rather than left to // be rediscovered: latched is "this filter is on" and focused is "you // are here", and there is one spare axis for two facts. let style = style(); let kind = Token::Chip { removable: false }; let latched = token(&style, "rust", kind, Tone::Neutral, true, false); let focused = token(&style, "rust", kind, Tone::Neutral, false, true); assert_eq!(latched.style, focused.style); assert!(latched.style.add_modifier.contains(Modifier::REVERSED)); } #[test] fn a_control_draws_its_key_only_where_one_was_named() { let style = style(); let line = act(&style, &Act::new("Delete"), false); assert_eq!(line.spans[0].content.as_ref(), "< Delete >"); let line = act(&style, &Act::new("Quit").key("q"), false); assert_eq!(line.spans[0].content.as_ref(), "< Quit > (q)"); } #[test] fn a_disabled_control_is_never_marked_focused() { // Present, visible, and not answering. A focus mark on it would be an // affordance that lies, so the flag is overridden rather than trusted. let style = style(); let disabled = Act::new("Save").state(State::Disabled); let line = act(&style, &disabled, true); assert!( !line.spans[0] .style .add_modifier .contains(Modifier::REVERSED) ); assert_eq!(line.spans[0].style, style.muted); // The same call on a control the description says nothing about: the // mark is this renderer's own focus flag and always was, which is why // only `Disabled` can override it. let unstated = Act::new("Save"); let line = act(&style, &unstated, true); assert!( line.spans[0] .style .add_modifier .contains(Modifier::REVERSED) ); } #[test] fn a_danger_control_keeps_its_tone_under_focus() { // Focus adds a modifier rather than repainting, so the fact that this // is the button that destroys something survives being landed on. let style = style(); let line = act(&style, &Act::new("Delete").tone(Tone::Danger), true); assert_eq!( line.spans[0].style.add_modifier, style.danger.add_modifier | Modifier::REVERSED ); } #[test] fn a_figure_puts_the_number_over_what_it_counts() { let style = style(); let figure_ = Figure::new("42", "open tasks"); let mut buf = buffer(20, 4); let used = figure(&style, &figure_, buf.area, &mut buf); assert_eq!(used, 2); assert_eq!(rows(&buf)[..2], ["42".to_owned(), "open tasks".to_owned()]); assert_eq!(figure_height(&figure_, 20), 2); } #[test] fn a_figures_change_rides_on_the_value_row() { // The delta is the toned part and the value is an ordinary fact, so the // two share a row rather than the caption growing a second sentence. let style = style(); let figure_ = Figure::new("42", "open tasks") .change("+3") .tone(Tone::Success); let mut buf = buffer(20, 4); figure(&style, &figure_, buf.area, &mut buf); assert_eq!(rows(&buf)[0], "42 +3"); } #[test] fn a_compulsory_field_says_so_in_its_label() { let style = style(); let mut field_ = Field::new(FieldKind::Text, "email", "Email"); field_.required = true; let mut buf = buffer(20, 4); field(&style, &field_, Held::Absent, false, buf.area, &mut buf); assert_eq!(rows(&buf)[0], "Email *"); } #[test] fn a_hidden_field_costs_no_rows_at_all() { // The one field kind a terminal and a webview agree on completely. let style = style(); let field_ = Field::new(FieldKind::Hidden, "csrf", "Token"); let mut buf = buffer(20, 4); assert_eq!( field( &style, &field_, Held::Text("abc"), false, buf.area, &mut buf ), 0 ); assert_eq!(field_height(&style, &field_, 20), 0); assert_eq!(rows(&buf)[0], ""); } #[test] fn a_secret_is_dotted_from_the_callers_buffer_and_never_from_the_description() { // The one control that would be undrawable without `held`: a password // that came back down the wire is a password in a page and in a log. let style = style(); let field_ = Field::new(FieldKind::Secret, "password", "Password"); let mut buf = buffer(20, 4); field( &style, &field_, Held::Text("hunter2"), false, buf.area, &mut buf, ); assert_eq!(rows(&buf)[1], "*******"); } #[test] fn an_error_takes_the_row_the_hint_would_have_had() { // Once something has gone wrong that is the sentence worth the row, // which is the order a webview uses too. let style = style(); let mut field_ = Field::new(FieldKind::Text, "email", "Email"); field_.hint = Some("work address"); field_.error = Some("not an address"); let mut buf = buffer(20, 5); field( &style, &field_, Held::Text("nope"), false, buf.area, &mut buf, ); assert_eq!(rows(&buf)[2], "not an address"); assert_eq!(field_height(&style, &field_, 20), 3); } #[test] fn a_focused_empty_box_shows_where_the_typing_will_land() { // An empty field under a style is an empty field. Without the caret a // focused box with no placeholder drew literally nothing. let style = style(); let field_ = Field::new(FieldKind::Text, "email", "Email"); let mut buf = buffer(20, 4); field(&style, &field_, Held::Absent, true, buf.area, &mut buf); let caret = buf.cell((0, 1)).expect("the well's first cell").style(); assert!(caret.add_modifier.contains(Modifier::REVERSED)); } #[test] fn a_choice_field_marks_the_chosen_option_and_costs_a_row_each() { let style = style(); let mut field_ = Field::new(FieldKind::Radio, "size", "Size"); let options = [Choice::plain("small"), Choice::plain("large")]; field_.options = &options; let mut buf = buffer(20, 5); field( &style, &field_, Held::Text("large"), false, buf.area, &mut buf, ); assert_eq!(rows(&buf)[1], "( ) small"); assert_eq!(rows(&buf)[2], "(*) large"); assert_eq!(field_height(&style, &field_, 20), 3); } #[test] fn a_range_draws_its_two_ends_and_where_the_value_sits_between_them() { let style = style(); let field_ = Field::range("review", "Review above", "0", "1"); let mut buf = buffer(40, 3); field( &style, &field_, Held::Text("0.5"), false, buf.area, &mut buf, ); // Ten cells by default, half of them filled, with the extent read out // at either side: 0.5 means nothing without the 0 and the 1. assert_eq!(rows(&buf)[1].trim_end(), "0 #####----- 1 0.5"); assert_eq!(field_height(&style, &field_, 40), 2); } #[test] fn a_unit_rides_on_the_value_and_not_on_the_label() { // The label is a line above; the number is the line the eye is on. let style = style(); let field_ = Field { unit: Some("s"), ..Field::range("attack", "Attack", "0", "5") }; let mut buf = buffer(40, 3); field( &style, &field_, Held::Text("2.5"), false, buf.area, &mut buf, ); assert_eq!(rows(&buf)[0].trim_end(), "Attack"); assert_eq!(rows(&buf)[1].trim_end(), "0 #####----- 5 2.5 s"); } #[test] fn a_typed_number_reads_with_its_unit_too() { let style = style(); let field_ = Field { unit: Some("ms"), ..Field::new(FieldKind::Number, "fade", "Fade") }; let mut buf = buffer(40, 3); field(&style, &field_, Held::Text("50"), false, buf.area, &mut buf); assert_eq!(rows(&buf)[1].trim_end(), "50 ms"); } #[test] fn a_unit_on_a_kind_that_is_not_a_quantity_is_ignored() { // Which kinds are quantities is the description's answer, not a // `matches!` kept in this crate. let style = style(); let field_ = Field { unit: Some("s"), ..Field::new(FieldKind::Text, "name", "Name") }; let mut buf = buffer(40, 3); field( &style, &field_, Held::Text("kick"), false, buf.area, &mut buf, ); assert_eq!(rows(&buf)[1].trim_end(), "kick"); } #[test] fn an_interval_is_one_line_with_both_ends_on_it() { // One question, one line. Two rows would read as two questions, which // is the reading the kind exists to prevent. let style = style(); let field_ = Field { min: Some("0"), max: Some("300"), unit: Some("BPM"), ..Field::interval("bpm_min", "bpm_max", "BPM range") }; let mut buf = buffer(40, 3); field( &style, &field_, Held::Between { lower: "90", upper: "130", }, false, buf.area, &mut buf, ); assert_eq!(rows(&buf)[0].trim_end(), "BPM range"); assert_eq!(rows(&buf)[1].trim_end(), "90 BPM to 130 BPM"); assert_eq!(rows(&buf)[2].trim_end(), ""); } #[test] fn an_open_end_falls_back_to_the_bound_it_means() { // "Over 120" is an answer rather than a half-filled box, and where the // axis ends is what the empty end stands for. let style = style(); let field_ = Field { min: Some("0"), max: Some("300"), ..Field::interval("bpm_min", "bpm_max", "BPM range") }; let mut buf = buffer(40, 3); field( &style, &field_, Held::Between { lower: "120", upper: "", }, false, buf.area, &mut buf, ); assert_eq!(rows(&buf)[1].trim_end(), "120 to 300"); } #[test] fn an_unbounded_open_end_draws_nothing_rather_than_a_number() { // A terminal inventing a bound here would report a filter nobody // applied, which is `range_line`'s position on an unreadable value. // What is left reads as the sentence it is: up to 130. let style = style(); let field_ = Field::interval("bpm_min", "bpm_max", "BPM range"); let mut buf = buffer(40, 3); field( &style, &field_, Held::Between { lower: "", upper: "130", }, false, buf.area, &mut buf, ); assert_eq!(rows(&buf)[1].trim_end(), "to 130"); } #[test] fn a_range_holding_something_unreadable_still_shows_it() { // The app put the value there. A terminal that quietly rounded it to a // bound would be reporting a value nobody set, which is `empty_well`'s // position on the same problem. let style = style(); let field_ = Field::range("review", "Review above", "0", "1"); let mut buf = buffer(40, 3); field( &style, &field_, Held::Text("unset"), false, buf.area, &mut buf, ); assert_eq!(rows(&buf)[1].trim_end(), "0 ---------- 1 unset"); } #[test] fn an_unbounded_range_is_typed_into_rather_than_dragged() { // Bounds this crate invented are bounds the user would then drag // against. The text path takes every answer the bar would. let style = style(); let field_ = Field { max: Some("1"), ..Field::new(FieldKind::Range, "review", "Review above") }; let mut buf = buffer(40, 3); field( &style, &field_, Held::Text("0.5"), false, buf.area, &mut buf, ); assert_eq!(rows(&buf)[1].trim_end(), "0.5"); } #[test] fn an_unavailable_option_reads_as_inert_and_says_why() { // The one place muted is the truth rather than the lie the convention // warns about: this option will not answer, and the reason is on the // row rather than nowhere. let style = style(); let options = [ Choice::new("chromatic", "Chromatic"), Choice::new("multi", "Multi-sample").unless("Drop a second sample."), ]; let mut field_ = Field::new(FieldKind::Radio, "mode", "Mode"); field_.options = &options; let mut buf = buffer(46, 4); field( &style, &field_, Held::Text("chromatic"), false, buf.area, &mut buf, ); assert_eq!(rows(&buf)[1].trim_end(), "(*) Chromatic"); assert_eq!( rows(&buf)[2].trim_end(), "( ) Multi-sample: Drop a second sample." ); let muted = buf.cell((0, 2)).expect("the unavailable row").style(); assert!(muted.add_modifier.contains(Modifier::DIM)); } #[test] fn an_option_can_carry_the_line_that_says_what_it_means() { // makeover-layout 0.39.0. A terminal has rows, so the line gets one of // its own under the option, indented past the mark and muted: it is not // a thing to press, which is the one reading muted is honest about. let style = style(); let options = [ Choice::new("16", "Basic").detailing("$16/mo. Fits text, blogs, newsletters."), Choice::new("24", "Small Files"), ]; let mut field_ = Field::new(FieldKind::Radio, "tier", "Tier"); field_.options = &options; let mut buf = buffer(46, 5); field(&style, &field_, Held::Text("16"), false, buf.area, &mut buf); let drawn = rows(&buf); assert_eq!(drawn[1].trim_end(), "(*) Basic"); assert_eq!( drawn[2].trim_end(), " $16/mo. Fits text, blogs, newsletters." ); // The next option follows the line rather than being pushed off: the // row count the drawing returns is what the caller lays out with. assert_eq!(drawn[3].trim_end(), "( ) Small Files"); let muted = buf.cell((4, 2)).expect("the detail row").style(); assert!(muted.add_modifier.contains(Modifier::DIM)); } #[test] fn an_unchosen_option_does_not_read_as_disabled() { // The three-tone convention: muted is inert, and every option in this // list answers a press. Drawn muted, a five-option radio read as one // live row and four dead ones. let style = style(); let mut field_ = Field::new(FieldKind::Radio, "size", "Size"); let options = [Choice::plain("small"), Choice::plain("large")]; field_.options = &options; let mut buf = buffer(20, 5); field( &style, &field_, Held::Text("large"), false, buf.area, &mut buf, ); let unchosen = buf.cell((0, 1)).expect("the first option").style(); assert_eq!(unchosen.add_modifier, style.secondary.add_modifier); assert_ne!(unchosen.add_modifier, style.muted.add_modifier); } #[test] fn a_checkbox_reads_a_bool_rather_than_a_submitted_string() { // `Held::On` exists so a host's own submission convention -- quasi // sends "value" -- stays the host's and never reaches a drawing. let style = style(); let field_ = Field::new(FieldKind::Checkbox, "agree", "Agree"); let mut buf = buffer(20, 4); field(&style, &field_, Held::On(true), false, buf.area, &mut buf); assert_eq!(rows(&buf)[1], "[x]"); let mut buf = buffer(20, 4); field(&style, &field_, Held::On(false), false, buf.area, &mut buf); assert_eq!(rows(&buf)[1], "[ ]"); } #[test] fn a_markdown_field_gets_the_rows_a_textarea_does() { // Keyed on `multiline`, so a member added upstream does not silently // land on the single-row arm. One row for a value whose whole point is // that it has several is the failure this replaced. let style = PieceStyle::default(); let rich = Field::new(FieldKind::Rich, "body", "Body"); let textarea = Field::new(FieldKind::Textarea, "body", "Body"); let plain = Field::new(FieldKind::Text, "body", "Body"); assert_eq!( field_height(&style, &rich, 40), field_height(&style, &textarea, 40) ); assert!(field_height(&style, &rich, 40) > field_height(&style, &plain, 40)); } #[test] fn a_tone_and_a_heading_map_without_a_fallback_arm() { // Both source enums are closed, which is what lets these be total. A // renderer that had to guess would be picking its own colours again. let style = style(); assert_eq!(style.tone(Tone::Neutral), style.content); assert_eq!(style.tone(Tone::Danger), style.danger); assert_eq!(style.heading(Heading::Page), style.page); assert_eq!(style.heading(Heading::Subsection), style.subsection); } #[test] fn the_default_style_carries_no_colour_at_all() { // A two-colour terminal is the case where a foreground will not land, // so the default is modifiers only rather than a placeholder palette. let style = PieceStyle::default(); for painted in [style.content, style.danger, style.page, style.action] { assert_eq!(painted.fg, None); assert_eq!(painted.bg, None); } } #[test] fn the_three_states_of_a_wait_are_three_drawings() { // The whole done condition of `5db1e0ed`: a measured wait and an // unmeasured one stopped being the same line. let style = PieceStyle::default(); let bare = awaiting(&style, Awaiting::unmeasured(), Progress::default(), true); let sized = awaiting(&style, Awaiting::of(41_943_040), Progress::default(), true); let watched = awaiting( &style, Awaiting::of(40), Progress { delivered: Some(20), elapsed: Some(Duration::from_secs(4)), }, true, ); let read = |line: &Line<'_>| { line.spans .iter() .map(|s| s.content.to_string()) .collect::() }; assert_eq!(read(&bare), "#"); assert_eq!(read(&sized), "# 41943040"); assert_eq!(read(&watched), "#####----- 20/40 4s"); } #[test] fn a_dark_mark_still_occupies_its_cell() { // Not absent. A line that reflowed every half second would move the // content beside it, and the reader would lose where to look. let style = PieceStyle::default(); assert_eq!(activity(&style, true).content.chars().count(), 1); assert_eq!(activity(&style, false).content.chars().count(), 1); } #[test] fn an_over_delivered_wait_clamps_and_does_not_panic() { // A transfer can hand over more than the size it announced, and the // bar has ten cells whatever happens. let style = PieceStyle::default(); let over = awaiting( &style, Awaiting::of(4), Progress { delivered: Some(9), elapsed: None, }, true, ); assert!(over.spans[0].content.chars().all(|c| c == '#')); assert_eq!(over.spans[0].content.chars().count(), 10); // A zero payload is no payload rather than a finished one. let empty = awaiting( &style, Awaiting::of(0), Progress { delivered: Some(9), elapsed: None, }, true, ); assert!(empty.spans[0].content.starts_with('-')); } #[test] fn a_theme_picker_heads_each_group_and_marks_each_tier() { const THEMES: &[makeover_layout::ThemeChoice<'_>] = &[ makeover_layout::ThemeChoice::new( "goingson", "GoingsOn", ThemeVariant::Light, makeover_layout::Contrast::High, ), makeover_layout::ThemeChoice::new( "carbonfox", "Carbonfox", ThemeVariant::Dark, makeover_layout::Contrast::Standard, ), ]; let style = style(); let field_ = Field::theme("theme", "Theme", THEMES) .following(makeover_layout::Choice::new("system", "Follow System")); let mut buf = buffer(32, 8); field( &style, &field_, Held::Text("carbonfox"), false, buf.area, &mut buf, ); let rows = rows(&buf); assert_eq!(rows[1], "( ) Follow System"); assert_eq!(rows[2], "Light"); assert_eq!(rows[3], "( ) GoingsOn [AA]"); assert_eq!(rows[4], "Dark"); assert_eq!(rows[5], "(*) Carbonfox [OK]"); } #[test] fn a_theme_picker_asks_for_the_rows_it_draws() { // Label, follow, two headings, two themes. A height that counted the // themes alone would clip the last group off every picker. const THEMES: &[makeover_layout::ThemeChoice<'_>] = &[ makeover_layout::ThemeChoice::new( "goingson", "GoingsOn", ThemeVariant::Light, makeover_layout::Contrast::High, ), makeover_layout::ThemeChoice::new( "carbonfox", "Carbonfox", ThemeVariant::Dark, makeover_layout::Contrast::Standard, ), ]; let style = style(); let field_ = Field::theme("theme", "Theme", THEMES) .following(makeover_layout::Choice::new("system", "Follow System")); assert_eq!(field_height(&style, &field_, 32), 6); // One variant, no follow row: one heading, not three. let one = Field::theme("theme", "Theme", &THEMES[..1]); assert_eq!(field_height(&style, &one, 32), 3); }