Skip to main content

max / makeover-tui

24.2 KB · 748 lines History Blame Raw
1 //! Tests for [`super`].
2
3 #[test]
4 fn one_line_takes_the_error_then_the_note_then_the_hint() {
5 // A terminal field has room for exactly one message, so the three
6 // channels compete and `Field::note` decides the order.
7 let style = PieceStyle::default();
8 let mut f = Field::new(FieldKind::Text, "title", "Title");
9 f.hint = Some("how it works");
10 assert_eq!(message_of(&style, &f).unwrap().0, "how it works");
11
12 f.note = Some((Tone::Warning, "what it costs"));
13 assert_eq!(message_of(&style, &f).unwrap().0, "what it costs");
14 assert_eq!(message_of(&style, &f).unwrap().1, style.warning);
15
16 f.error = Some("what is wrong");
17 assert_eq!(message_of(&style, &f).unwrap().0, "what is wrong");
18 assert_eq!(message_of(&style, &f).unwrap().1, style.danger);
19
20 // A note carries its own tone, so a quiet one is not painted as a
21 // warning just for being a note.
22 f.error = None;
23 f.note = Some((Tone::Neutral, "an ordinary fact"));
24 assert_eq!(message_of(&style, &f).unwrap().1, style.content);
25 }
26 use super::*;
27 use makeover_layout::{Choice, State};
28
29 /// The style the drawings are read against: one distinguishable modifier
30 /// per role, so a test can say which style landed without a colour.
31 fn style() -> PieceStyle {
32 PieceStyle {
33 content: Style::new().add_modifier(Modifier::BOLD),
34 secondary: Style::new().add_modifier(Modifier::ITALIC),
35 muted: Style::new().add_modifier(Modifier::DIM),
36 danger: Style::new().add_modifier(Modifier::CROSSED_OUT),
37 ..PieceStyle::default()
38 }
39 }
40
41 fn buffer(width: u16, height: u16) -> Buffer {
42 Buffer::empty(Rect::new(0, 0, width, height))
43 }
44
45 /// Everything in the buffer, one string per row.
46 fn rows(buf: &Buffer) -> Vec<String> {
47 (0..buf.area.height)
48 .map(|y| {
49 (0..buf.area.width)
50 .map(|x| {
51 buf.cell((x, y))
52 .map_or(' ', |c| c.symbol().chars().next().unwrap_or(' '))
53 })
54 .collect::<String>()
55 .trim_end()
56 .to_owned()
57 })
58 .collect()
59 }
60
61 #[test]
62 fn a_bar_fills_in_proportion_and_reads_out_the_two_numbers() {
63 let style = style();
64 let line = meter(&style, &Meter::new(3, 10).label("subtasks"));
65 let drawn: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
66 assert_eq!(drawn, "###------- 3/10 subtasks");
67 // The noun is optional and the ratio is not, because a bar with no
68 // reading is a bar you cannot check.
69 let bare = meter(&style, &Meter::new(3, 10));
70 let drawn: String = bare.spans.iter().map(|s| s.content.as_ref()).collect();
71 assert_eq!(drawn, "###------- 3/10");
72 }
73
74 #[test]
75 fn an_empty_set_is_an_empty_bar_rather_than_a_divide_by_zero() {
76 // `Meter::total` of zero means there is no set, and the checked
77 // division is what keeps that from being a panic in a draw.
78 let line = meter(&style(), &Meter::new(0, 0));
79 let drawn: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
80 assert_eq!(drawn, "---------- 0/0");
81 }
82
83 #[test]
84 fn an_over_run_fills_the_bar_and_still_reports_the_overflow() {
85 // The clamp is for drawing only. The reading is what keeps the fact
86 // `Meter::percent` destroys.
87 let line = meter(&style(), &Meter::new(14, 10));
88 let drawn: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
89 assert_eq!(drawn, "########## 14/10");
90 }
91
92 #[test]
93 fn a_badge_is_round_and_a_chip_is_square() {
94 // The one affordance a cell has left once colour is spent on the tone,
95 // and the whole of how a terminal says "this one answers a press".
96 let style = style();
97 let badge = token(&style, "draft", Token::Badge, Tone::Neutral, false, false);
98 assert_eq!(badge.content.as_ref(), "(draft)");
99 let chip = token(
100 &style,
101 "rust",
102 Token::Chip { removable: false },
103 Tone::Neutral,
104 false,
105 false,
106 );
107 assert_eq!(chip.content.as_ref(), "[rust]");
108 }
109
110 #[test]
111 fn a_latched_chip_reads_the_same_as_a_focused_one() {
112 // The collision a terminal cannot avoid, asserted rather than left to
113 // be rediscovered: latched is "this filter is on" and focused is "you
114 // are here", and there is one spare axis for two facts.
115 let style = style();
116 let kind = Token::Chip { removable: false };
117 let latched = token(&style, "rust", kind, Tone::Neutral, true, false);
118 let focused = token(&style, "rust", kind, Tone::Neutral, false, true);
119 assert_eq!(latched.style, focused.style);
120 assert!(latched.style.add_modifier.contains(Modifier::REVERSED));
121 }
122
123 #[test]
124 fn a_control_draws_its_key_only_where_one_was_named() {
125 let style = style();
126 let line = act(&style, &Act::new("Delete"), false);
127 assert_eq!(line.spans[0].content.as_ref(), "< Delete >");
128 let line = act(&style, &Act::new("Quit").key("q"), false);
129 assert_eq!(line.spans[0].content.as_ref(), "< Quit > (q)");
130 }
131
132 #[test]
133 fn a_disabled_control_is_never_marked_focused() {
134 // Present, visible, and not answering. A focus mark on it would be an
135 // affordance that lies, so the flag is overridden rather than trusted.
136 let style = style();
137 let disabled = Act::new("Save").state(State::Disabled);
138 let line = act(&style, &disabled, true);
139 assert!(
140 !line.spans[0]
141 .style
142 .add_modifier
143 .contains(Modifier::REVERSED)
144 );
145 assert_eq!(line.spans[0].style, style.muted);
146 // The same call on a control the description says nothing about: the
147 // mark is this renderer's own focus flag and always was, which is why
148 // only `Disabled` can override it.
149 let unstated = Act::new("Save");
150 let line = act(&style, &unstated, true);
151 assert!(
152 line.spans[0]
153 .style
154 .add_modifier
155 .contains(Modifier::REVERSED)
156 );
157 }
158
159 #[test]
160 fn a_danger_control_keeps_its_tone_under_focus() {
161 // Focus adds a modifier rather than repainting, so the fact that this
162 // is the button that destroys something survives being landed on.
163 let style = style();
164 let line = act(&style, &Act::new("Delete").tone(Tone::Danger), true);
165 assert_eq!(
166 line.spans[0].style.add_modifier,
167 style.danger.add_modifier | Modifier::REVERSED
168 );
169 }
170
171 #[test]
172 fn a_figure_puts_the_number_over_what_it_counts() {
173 let style = style();
174 let figure_ = Figure::new("42", "open tasks");
175 let mut buf = buffer(20, 4);
176 let used = figure(&style, &figure_, buf.area, &mut buf);
177 assert_eq!(used, 2);
178 assert_eq!(rows(&buf)[..2], ["42".to_owned(), "open tasks".to_owned()]);
179 assert_eq!(figure_height(&figure_, 20), 2);
180 }
181
182 #[test]
183 fn a_figures_change_rides_on_the_value_row() {
184 // The delta is the toned part and the value is an ordinary fact, so the
185 // two share a row rather than the caption growing a second sentence.
186 let style = style();
187 let figure_ = Figure::new("42", "open tasks")
188 .change("+3")
189 .tone(Tone::Success);
190 let mut buf = buffer(20, 4);
191 figure(&style, &figure_, buf.area, &mut buf);
192 assert_eq!(rows(&buf)[0], "42 +3");
193 }
194
195 #[test]
196 fn a_compulsory_field_says_so_in_its_label() {
197 let style = style();
198 let mut field_ = Field::new(FieldKind::Text, "email", "Email");
199 field_.required = true;
200 let mut buf = buffer(20, 4);
201 field(&style, &field_, Held::Absent, false, buf.area, &mut buf);
202 assert_eq!(rows(&buf)[0], "Email *");
203 }
204
205 #[test]
206 fn a_hidden_field_costs_no_rows_at_all() {
207 // The one field kind a terminal and a webview agree on completely.
208 let style = style();
209 let field_ = Field::new(FieldKind::Hidden, "csrf", "Token");
210 let mut buf = buffer(20, 4);
211 assert_eq!(
212 field(
213 &style,
214 &field_,
215 Held::Text("abc"),
216 false,
217 buf.area,
218 &mut buf
219 ),
220 0
221 );
222 assert_eq!(field_height(&style, &field_, 20), 0);
223 assert_eq!(rows(&buf)[0], "");
224 }
225
226 #[test]
227 fn a_secret_is_dotted_from_the_callers_buffer_and_never_from_the_description() {
228 // The one control that would be undrawable without `held`: a password
229 // that came back down the wire is a password in a page and in a log.
230 let style = style();
231 let field_ = Field::new(FieldKind::Secret, "password", "Password");
232 let mut buf = buffer(20, 4);
233 field(
234 &style,
235 &field_,
236 Held::Text("hunter2"),
237 false,
238 buf.area,
239 &mut buf,
240 );
241 assert_eq!(rows(&buf)[1], "*******");
242 }
243
244 #[test]
245 fn an_error_takes_the_row_the_hint_would_have_had() {
246 // Once something has gone wrong that is the sentence worth the row,
247 // which is the order a webview uses too.
248 let style = style();
249 let mut field_ = Field::new(FieldKind::Text, "email", "Email");
250 field_.hint = Some("work address");
251 field_.error = Some("not an address");
252 let mut buf = buffer(20, 5);
253 field(
254 &style,
255 &field_,
256 Held::Text("nope"),
257 false,
258 buf.area,
259 &mut buf,
260 );
261 assert_eq!(rows(&buf)[2], "not an address");
262 assert_eq!(field_height(&style, &field_, 20), 3);
263 }
264
265 #[test]
266 fn a_focused_empty_box_shows_where_the_typing_will_land() {
267 // An empty field under a style is an empty field. Without the caret a
268 // focused box with no placeholder drew literally nothing.
269 let style = style();
270 let field_ = Field::new(FieldKind::Text, "email", "Email");
271 let mut buf = buffer(20, 4);
272 field(&style, &field_, Held::Absent, true, buf.area, &mut buf);
273 let caret = buf.cell((0, 1)).expect("the well's first cell").style();
274 assert!(caret.add_modifier.contains(Modifier::REVERSED));
275 }
276
277 #[test]
278 fn a_choice_field_marks_the_chosen_option_and_costs_a_row_each() {
279 let style = style();
280 let mut field_ = Field::new(FieldKind::Radio, "size", "Size");
281 let options = [Choice::plain("small"), Choice::plain("large")];
282 field_.options = &options;
283 let mut buf = buffer(20, 5);
284 field(
285 &style,
286 &field_,
287 Held::Text("large"),
288 false,
289 buf.area,
290 &mut buf,
291 );
292 assert_eq!(rows(&buf)[1], "( ) small");
293 assert_eq!(rows(&buf)[2], "(*) large");
294 assert_eq!(field_height(&style, &field_, 20), 3);
295 }
296
297 #[test]
298 fn a_range_draws_its_two_ends_and_where_the_value_sits_between_them() {
299 let style = style();
300 let field_ = Field::range("review", "Review above", "0", "1");
301 let mut buf = buffer(40, 3);
302 field(
303 &style,
304 &field_,
305 Held::Text("0.5"),
306 false,
307 buf.area,
308 &mut buf,
309 );
310 // Ten cells by default, half of them filled, with the extent read out
311 // at either side: 0.5 means nothing without the 0 and the 1.
312 assert_eq!(rows(&buf)[1].trim_end(), "0 #####----- 1 0.5");
313 assert_eq!(field_height(&style, &field_, 40), 2);
314 }
315
316 #[test]
317 fn a_unit_rides_on_the_value_and_not_on_the_label() {
318 // The label is a line above; the number is the line the eye is on.
319 let style = style();
320 let field_ = Field {
321 unit: Some("s"),
322 ..Field::range("attack", "Attack", "0", "5")
323 };
324 let mut buf = buffer(40, 3);
325 field(
326 &style,
327 &field_,
328 Held::Text("2.5"),
329 false,
330 buf.area,
331 &mut buf,
332 );
333 assert_eq!(rows(&buf)[0].trim_end(), "Attack");
334 assert_eq!(rows(&buf)[1].trim_end(), "0 #####----- 5 2.5 s");
335 }
336
337 #[test]
338 fn a_typed_number_reads_with_its_unit_too() {
339 let style = style();
340 let field_ = Field {
341 unit: Some("ms"),
342 ..Field::new(FieldKind::Number, "fade", "Fade")
343 };
344 let mut buf = buffer(40, 3);
345 field(&style, &field_, Held::Text("50"), false, buf.area, &mut buf);
346 assert_eq!(rows(&buf)[1].trim_end(), "50 ms");
347 }
348
349 #[test]
350 fn a_unit_on_a_kind_that_is_not_a_quantity_is_ignored() {
351 // Which kinds are quantities is the description's answer, not a
352 // `matches!` kept in this crate.
353 let style = style();
354 let field_ = Field {
355 unit: Some("s"),
356 ..Field::new(FieldKind::Text, "name", "Name")
357 };
358 let mut buf = buffer(40, 3);
359 field(
360 &style,
361 &field_,
362 Held::Text("kick"),
363 false,
364 buf.area,
365 &mut buf,
366 );
367 assert_eq!(rows(&buf)[1].trim_end(), "kick");
368 }
369
370 #[test]
371 fn an_interval_is_one_line_with_both_ends_on_it() {
372 // One question, one line. Two rows would read as two questions, which
373 // is the reading the kind exists to prevent.
374 let style = style();
375 let field_ = Field {
376 min: Some("0"),
377 max: Some("300"),
378 unit: Some("BPM"),
379 ..Field::interval("bpm_min", "bpm_max", "BPM range")
380 };
381 let mut buf = buffer(40, 3);
382 field(
383 &style,
384 &field_,
385 Held::Between {
386 lower: "90",
387 upper: "130",
388 },
389 false,
390 buf.area,
391 &mut buf,
392 );
393 assert_eq!(rows(&buf)[0].trim_end(), "BPM range");
394 assert_eq!(rows(&buf)[1].trim_end(), "90 BPM to 130 BPM");
395 assert_eq!(rows(&buf)[2].trim_end(), "");
396 }
397
398 #[test]
399 fn an_open_end_falls_back_to_the_bound_it_means() {
400 // "Over 120" is an answer rather than a half-filled box, and where the
401 // axis ends is what the empty end stands for.
402 let style = style();
403 let field_ = Field {
404 min: Some("0"),
405 max: Some("300"),
406 ..Field::interval("bpm_min", "bpm_max", "BPM range")
407 };
408 let mut buf = buffer(40, 3);
409 field(
410 &style,
411 &field_,
412 Held::Between {
413 lower: "120",
414 upper: "",
415 },
416 false,
417 buf.area,
418 &mut buf,
419 );
420 assert_eq!(rows(&buf)[1].trim_end(), "120 to 300");
421 }
422
423 #[test]
424 fn an_unbounded_open_end_draws_nothing_rather_than_a_number() {
425 // A terminal inventing a bound here would report a filter nobody
426 // applied, which is `range_line`'s position on an unreadable value.
427 // What is left reads as the sentence it is: up to 130.
428 let style = style();
429 let field_ = Field::interval("bpm_min", "bpm_max", "BPM range");
430 let mut buf = buffer(40, 3);
431 field(
432 &style,
433 &field_,
434 Held::Between {
435 lower: "",
436 upper: "130",
437 },
438 false,
439 buf.area,
440 &mut buf,
441 );
442 assert_eq!(rows(&buf)[1].trim_end(), "to 130");
443 }
444
445 #[test]
446 fn a_range_holding_something_unreadable_still_shows_it() {
447 // The app put the value there. A terminal that quietly rounded it to a
448 // bound would be reporting a value nobody set, which is `empty_well`'s
449 // position on the same problem.
450 let style = style();
451 let field_ = Field::range("review", "Review above", "0", "1");
452 let mut buf = buffer(40, 3);
453 field(
454 &style,
455 &field_,
456 Held::Text("unset"),
457 false,
458 buf.area,
459 &mut buf,
460 );
461 assert_eq!(rows(&buf)[1].trim_end(), "0 ---------- 1 unset");
462 }
463
464 #[test]
465 fn an_unbounded_range_is_typed_into_rather_than_dragged() {
466 // Bounds this crate invented are bounds the user would then drag
467 // against. The text path takes every answer the bar would.
468 let style = style();
469 let field_ = Field {
470 max: Some("1"),
471 ..Field::new(FieldKind::Range, "review", "Review above")
472 };
473 let mut buf = buffer(40, 3);
474 field(
475 &style,
476 &field_,
477 Held::Text("0.5"),
478 false,
479 buf.area,
480 &mut buf,
481 );
482 assert_eq!(rows(&buf)[1].trim_end(), "0.5");
483 }
484
485 #[test]
486 fn an_unavailable_option_reads_as_inert_and_says_why() {
487 // The one place muted is the truth rather than the lie the convention
488 // warns about: this option will not answer, and the reason is on the
489 // row rather than nowhere.
490 let style = style();
491 let options = [
492 Choice::new("chromatic", "Chromatic"),
493 Choice::new("multi", "Multi-sample").unless("Drop a second sample."),
494 ];
495 let mut field_ = Field::new(FieldKind::Radio, "mode", "Mode");
496 field_.options = &options;
497 let mut buf = buffer(46, 4);
498 field(
499 &style,
500 &field_,
501 Held::Text("chromatic"),
502 false,
503 buf.area,
504 &mut buf,
505 );
506 assert_eq!(rows(&buf)[1].trim_end(), "(*) Chromatic");
507 assert_eq!(
508 rows(&buf)[2].trim_end(),
509 "( ) Multi-sample: Drop a second sample."
510 );
511 let muted = buf.cell((0, 2)).expect("the unavailable row").style();
512 assert!(muted.add_modifier.contains(Modifier::DIM));
513 }
514
515 #[test]
516 fn an_option_can_carry_the_line_that_says_what_it_means() {
517 // makeover-layout 0.39.0. A terminal has rows, so the line gets one of
518 // its own under the option, indented past the mark and muted: it is not
519 // a thing to press, which is the one reading muted is honest about.
520 let style = style();
521 let options = [
522 Choice::new("16", "Basic").detailing("$16/mo. Fits text, blogs, newsletters."),
523 Choice::new("24", "Small Files"),
524 ];
525 let mut field_ = Field::new(FieldKind::Radio, "tier", "Tier");
526 field_.options = &options;
527 let mut buf = buffer(46, 5);
528 field(&style, &field_, Held::Text("16"), false, buf.area, &mut buf);
529
530 let drawn = rows(&buf);
531 assert_eq!(drawn[1].trim_end(), "(*) Basic");
532 assert_eq!(
533 drawn[2].trim_end(),
534 " $16/mo. Fits text, blogs, newsletters."
535 );
536 // The next option follows the line rather than being pushed off: the
537 // row count the drawing returns is what the caller lays out with.
538 assert_eq!(drawn[3].trim_end(), "( ) Small Files");
539 let muted = buf.cell((4, 2)).expect("the detail row").style();
540 assert!(muted.add_modifier.contains(Modifier::DIM));
541 }
542
543 #[test]
544 fn an_unchosen_option_does_not_read_as_disabled() {
545 // The three-tone convention: muted is inert, and every option in this
546 // list answers a press. Drawn muted, a five-option radio read as one
547 // live row and four dead ones.
548 let style = style();
549 let mut field_ = Field::new(FieldKind::Radio, "size", "Size");
550 let options = [Choice::plain("small"), Choice::plain("large")];
551 field_.options = &options;
552 let mut buf = buffer(20, 5);
553 field(
554 &style,
555 &field_,
556 Held::Text("large"),
557 false,
558 buf.area,
559 &mut buf,
560 );
561 let unchosen = buf.cell((0, 1)).expect("the first option").style();
562 assert_eq!(unchosen.add_modifier, style.secondary.add_modifier);
563 assert_ne!(unchosen.add_modifier, style.muted.add_modifier);
564 }
565
566 #[test]
567 fn a_checkbox_reads_a_bool_rather_than_a_submitted_string() {
568 // `Held::On` exists so a host's own submission convention -- quasi
569 // sends "value" -- stays the host's and never reaches a drawing.
570 let style = style();
571 let field_ = Field::new(FieldKind::Checkbox, "agree", "Agree");
572 let mut buf = buffer(20, 4);
573 field(&style, &field_, Held::On(true), false, buf.area, &mut buf);
574 assert_eq!(rows(&buf)[1], "[x]");
575 let mut buf = buffer(20, 4);
576 field(&style, &field_, Held::On(false), false, buf.area, &mut buf);
577 assert_eq!(rows(&buf)[1], "[ ]");
578 }
579
580 #[test]
581 fn a_markdown_field_gets_the_rows_a_textarea_does() {
582 // Keyed on `multiline`, so a member added upstream does not silently
583 // land on the single-row arm. One row for a value whose whole point is
584 // that it has several is the failure this replaced.
585 let style = PieceStyle::default();
586 let rich = Field::new(FieldKind::Rich, "body", "Body");
587 let textarea = Field::new(FieldKind::Textarea, "body", "Body");
588 let plain = Field::new(FieldKind::Text, "body", "Body");
589
590 assert_eq!(
591 field_height(&style, &rich, 40),
592 field_height(&style, &textarea, 40)
593 );
594 assert!(field_height(&style, &rich, 40) > field_height(&style, &plain, 40));
595 }
596
597 #[test]
598 fn a_tone_and_a_heading_map_without_a_fallback_arm() {
599 // Both source enums are closed, which is what lets these be total. A
600 // renderer that had to guess would be picking its own colours again.
601 let style = style();
602 assert_eq!(style.tone(Tone::Neutral), style.content);
603 assert_eq!(style.tone(Tone::Danger), style.danger);
604 assert_eq!(style.heading(Heading::Page), style.page);
605 assert_eq!(style.heading(Heading::Subsection), style.subsection);
606 }
607
608 #[test]
609 fn the_default_style_carries_no_colour_at_all() {
610 // A two-colour terminal is the case where a foreground will not land,
611 // so the default is modifiers only rather than a placeholder palette.
612 let style = PieceStyle::default();
613 for painted in [style.content, style.danger, style.page, style.action] {
614 assert_eq!(painted.fg, None);
615 assert_eq!(painted.bg, None);
616 }
617 }
618
619 #[test]
620 fn the_three_states_of_a_wait_are_three_drawings() {
621 // The whole done condition of `5db1e0ed`: a measured wait and an
622 // unmeasured one stopped being the same line.
623 let style = PieceStyle::default();
624 let bare = awaiting(&style, Awaiting::unmeasured(), Progress::default(), true);
625 let sized = awaiting(&style, Awaiting::of(41_943_040), Progress::default(), true);
626 let watched = awaiting(
627 &style,
628 Awaiting::of(40),
629 Progress {
630 delivered: Some(20),
631 elapsed: Some(Duration::from_secs(4)),
632 },
633 true,
634 );
635 let read = |line: &Line<'_>| {
636 line.spans
637 .iter()
638 .map(|s| s.content.to_string())
639 .collect::<String>()
640 };
641 assert_eq!(read(&bare), "#");
642 assert_eq!(read(&sized), "# 41943040");
643 assert_eq!(read(&watched), "#####----- 20/40 4s");
644 }
645
646 #[test]
647 fn a_dark_mark_still_occupies_its_cell() {
648 // Not absent. A line that reflowed every half second would move the
649 // content beside it, and the reader would lose where to look.
650 let style = PieceStyle::default();
651 assert_eq!(activity(&style, true).content.chars().count(), 1);
652 assert_eq!(activity(&style, false).content.chars().count(), 1);
653 }
654
655 #[test]
656 fn an_over_delivered_wait_clamps_and_does_not_panic() {
657 // A transfer can hand over more than the size it announced, and the
658 // bar has ten cells whatever happens.
659 let style = PieceStyle::default();
660 let over = awaiting(
661 &style,
662 Awaiting::of(4),
663 Progress {
664 delivered: Some(9),
665 elapsed: None,
666 },
667 true,
668 );
669 assert!(over.spans[0].content.chars().all(|c| c == '#'));
670 assert_eq!(over.spans[0].content.chars().count(), 10);
671 // A zero payload is no payload rather than a finished one.
672 let empty = awaiting(
673 &style,
674 Awaiting::of(0),
675 Progress {
676 delivered: Some(9),
677 elapsed: None,
678 },
679 true,
680 );
681 assert!(empty.spans[0].content.starts_with('-'));
682 }
683
684 #[test]
685 fn a_theme_picker_heads_each_group_and_marks_each_tier() {
686 const THEMES: &[makeover_layout::ThemeChoice<'_>] = &[
687 makeover_layout::ThemeChoice::new(
688 "goingson",
689 "GoingsOn",
690 ThemeVariant::Light,
691 makeover_layout::Contrast::High,
692 ),
693 makeover_layout::ThemeChoice::new(
694 "carbonfox",
695 "Carbonfox",
696 ThemeVariant::Dark,
697 makeover_layout::Contrast::Standard,
698 ),
699 ];
700 let style = style();
701 let field_ = Field::theme("theme", "Theme", THEMES)
702 .following(makeover_layout::Choice::new("system", "Follow System"));
703 let mut buf = buffer(32, 8);
704 field(
705 &style,
706 &field_,
707 Held::Text("carbonfox"),
708 false,
709 buf.area,
710 &mut buf,
711 );
712
713 let rows = rows(&buf);
714 assert_eq!(rows[1], "( ) Follow System");
715 assert_eq!(rows[2], "Light");
716 assert_eq!(rows[3], "( ) GoingsOn [AA]");
717 assert_eq!(rows[4], "Dark");
718 assert_eq!(rows[5], "(*) Carbonfox [OK]");
719 }
720
721 #[test]
722 fn a_theme_picker_asks_for_the_rows_it_draws() {
723 // Label, follow, two headings, two themes. A height that counted the
724 // themes alone would clip the last group off every picker.
725 const THEMES: &[makeover_layout::ThemeChoice<'_>] = &[
726 makeover_layout::ThemeChoice::new(
727 "goingson",
728 "GoingsOn",
729 ThemeVariant::Light,
730 makeover_layout::Contrast::High,
731 ),
732 makeover_layout::ThemeChoice::new(
733 "carbonfox",
734 "Carbonfox",
735 ThemeVariant::Dark,
736 makeover_layout::Contrast::Standard,
737 ),
738 ];
739 let style = style();
740 let field_ = Field::theme("theme", "Theme", THEMES)
741 .following(makeover_layout::Choice::new("system", "Follow System"));
742 assert_eq!(field_height(&style, &field_, 32), 6);
743
744 // One variant, no follow row: one heading, not three.
745 let one = Field::theme("theme", "Theme", &THEMES[..1]);
746 assert_eq!(field_height(&style, &one, 32), 3);
747 }
748