Skip to main content

max / makeover-tui

Move piece's test module to a sibling file piece.rs goes 1740 lines to 993, its 38 tests moving to src/piece/tests.rs. No public path moved, so this needs no version bump and no publish.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01EEmeiSJnmyL98QzA5Dwsvz
Author: Max Johnson <me@maxj.phd> · 2026-09-03 23:24 UTC
Signed with PGP, not checked
Commit: b7ea670a320ea52da52bd6715562084ec97da512
Parent: 838b5fb
2 files changed, +500 insertions, -497 deletions
M src/piece.rs -497
@@ -990,751 +990,4 @@
990 990 }
991 991
992 992 #[cfg(test)]
993 - mod tests {
994 -
995 - #[test]
996 - fn one_line_takes_the_error_then_the_note_then_the_hint() {
997 - // A terminal field has room for exactly one message, so the three
998 - // channels compete and `Field::note` decides the order.
999 - let style = PieceStyle::default();
1000 - let mut f = Field::new(FieldKind::Text, "title", "Title");
1001 - f.hint = Some("how it works");
1002 - assert_eq!(message_of(&style, &f).unwrap().0, "how it works");
1003 -
1004 - f.note = Some((Tone::Warning, "what it costs"));
1005 - assert_eq!(message_of(&style, &f).unwrap().0, "what it costs");
1006 - assert_eq!(message_of(&style, &f).unwrap().1, style.warning);
1007 -
1008 - f.error = Some("what is wrong");
1009 - assert_eq!(message_of(&style, &f).unwrap().0, "what is wrong");
1010 - assert_eq!(message_of(&style, &f).unwrap().1, style.danger);
1011 -
1012 - // A note carries its own tone, so a quiet one is not painted as a
1013 - // warning just for being a note.
1014 - f.error = None;
1015 - f.note = Some((Tone::Neutral, "an ordinary fact"));
1016 - assert_eq!(message_of(&style, &f).unwrap().1, style.content);
1017 - }
1018 - use super::*;
1019 - use makeover_layout::{Choice, State};
1020 -
1021 - /// The style the drawings are read against: one distinguishable modifier
1022 - /// per role, so a test can say which style landed without a colour.
1023 - fn style() -> PieceStyle {
1024 - PieceStyle {
1025 - content: Style::new().add_modifier(Modifier::BOLD),
1026 - secondary: Style::new().add_modifier(Modifier::ITALIC),
1027 - muted: Style::new().add_modifier(Modifier::DIM),
1028 - danger: Style::new().add_modifier(Modifier::CROSSED_OUT),
1029 - ..PieceStyle::default()
1030 - }
1031 - }
1032 -
1033 - fn buffer(width: u16, height: u16) -> Buffer {
1034 - Buffer::empty(Rect::new(0, 0, width, height))
1035 - }
1036 -
1037 - /// Everything in the buffer, one string per row.
1038 - fn rows(buf: &Buffer) -> Vec<String> {
1039 - (0..buf.area.height)
1040 - .map(|y| {
1041 - (0..buf.area.width)
1042 - .map(|x| {
1043 - buf.cell((x, y))
1044 - .map_or(' ', |c| c.symbol().chars().next().unwrap_or(' '))
1045 - })
1046 - .collect::<String>()
1047 - .trim_end()
1048 - .to_owned()
1049 - })
1050 - .collect()
1051 - }
1052 -
1053 - #[test]
1054 - fn a_bar_fills_in_proportion_and_reads_out_the_two_numbers() {
1055 - let style = style();
1056 - let line = meter(&style, &Meter::new(3, 10).label("subtasks"));
1057 - let drawn: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
1058 - assert_eq!(drawn, "###------- 3/10 subtasks");
1059 - // The noun is optional and the ratio is not, because a bar with no
1060 - // reading is a bar you cannot check.
1061 - let bare = meter(&style, &Meter::new(3, 10));
1062 - let drawn: String = bare.spans.iter().map(|s| s.content.as_ref()).collect();
1063 - assert_eq!(drawn, "###------- 3/10");
1064 - }
1065 -
1066 - #[test]
1067 - fn an_empty_set_is_an_empty_bar_rather_than_a_divide_by_zero() {
1068 - // `Meter::total` of zero means there is no set, and the checked
1069 - // division is what keeps that from being a panic in a draw.
1070 - let line = meter(&style(), &Meter::new(0, 0));
1071 - let drawn: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
1072 - assert_eq!(drawn, "---------- 0/0");
1073 - }
1074 -
1075 - #[test]
1076 - fn an_over_run_fills_the_bar_and_still_reports_the_overflow() {
1077 - // The clamp is for drawing only. The reading is what keeps the fact
1078 - // `Meter::percent` destroys.
1079 - let line = meter(&style(), &Meter::new(14, 10));
1080 - let drawn: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
1081 - assert_eq!(drawn, "########## 14/10");
1082 - }
1083 -
1084 - #[test]
1085 - fn a_badge_is_round_and_a_chip_is_square() {
1086 - // The one affordance a cell has left once colour is spent on the tone,
1087 - // and the whole of how a terminal says "this one answers a press".
1088 - let style = style();
1089 - let badge = token(&style, "draft", Token::Badge, Tone::Neutral, false, false);
1090 - assert_eq!(badge.content.as_ref(), "(draft)");
1091 - let chip = token(
1092 - &style,
1093 - "rust",
1094 - Token::Chip { removable: false },
1095 - Tone::Neutral,
1096 - false,
1097 - false,
1098 - );
1099 - assert_eq!(chip.content.as_ref(), "[rust]");
1100 - }
1101 -
1102 - #[test]
1103 - fn a_latched_chip_reads_the_same_as_a_focused_one() {
1104 - // The collision a terminal cannot avoid, asserted rather than left to
1105 - // be rediscovered: latched is "this filter is on" and focused is "you
1106 - // are here", and there is one spare axis for two facts.
1107 - let style = style();
1108 - let kind = Token::Chip { removable: false };
1109 - let latched = token(&style, "rust", kind, Tone::Neutral, true, false);
1110 - let focused = token(&style, "rust", kind, Tone::Neutral, false, true);
1111 - assert_eq!(latched.style, focused.style);
1112 - assert!(latched.style.add_modifier.contains(Modifier::REVERSED));
1113 - }
1114 -
1115 - #[test]
1116 - fn a_control_draws_its_key_only_where_one_was_named() {
1117 - let style = style();
1118 - let line = act(&style, &Act::new("Delete"), false);
1119 - assert_eq!(line.spans[0].content.as_ref(), "< Delete >");
1120 - let line = act(&style, &Act::new("Quit").key("q"), false);
1121 - assert_eq!(line.spans[0].content.as_ref(), "< Quit > (q)");
1122 - }
1123 -
1124 - #[test]
1125 - fn a_disabled_control_is_never_marked_focused() {
1126 - // Present, visible, and not answering. A focus mark on it would be an
1127 - // affordance that lies, so the flag is overridden rather than trusted.
1128 - let style = style();
1129 - let disabled = Act::new("Save").state(State::Disabled);
1130 - let line = act(&style, &disabled, true);
1131 - assert!(
1132 - !line.spans[0]
1133 - .style
1134 - .add_modifier
1135 - .contains(Modifier::REVERSED)
1136 - );
1137 - assert_eq!(line.spans[0].style, style.muted);
1138 - // The same call on a control the description says nothing about: the
1139 - // mark is this renderer's own focus flag and always was, which is why
1140 - // only `Disabled` can override it.
1141 - let unstated = Act::new("Save");
1142 - let line = act(&style, &unstated, true);
1143 - assert!(
1144 - line.spans[0]
1145 - .style
1146 - .add_modifier
1147 - .contains(Modifier::REVERSED)
1148 - );
1149 - }
1150 -
1151 - #[test]
1152 - fn a_danger_control_keeps_its_tone_under_focus() {
1153 - // Focus adds a modifier rather than repainting, so the fact that this
1154 - // is the button that destroys something survives being landed on.
1155 - let style = style();
1156 - let line = act(&style, &Act::new("Delete").tone(Tone::Danger), true);
1157 - assert_eq!(
1158 - line.spans[0].style.add_modifier,
1159 - style.danger.add_modifier | Modifier::REVERSED
1160 - );
1161 - }
1162 -
1163 - #[test]
1164 - fn a_figure_puts_the_number_over_what_it_counts() {
1165 - let style = style();
1166 - let figure_ = Figure::new("42", "open tasks");
1167 - let mut buf = buffer(20, 4);
1168 - let used = figure(&style, &figure_, buf.area, &mut buf);
1169 - assert_eq!(used, 2);
1170 - assert_eq!(rows(&buf)[..2], ["42".to_owned(), "open tasks".to_owned()]);
1171 - assert_eq!(figure_height(&figure_, 20), 2);
1172 - }
1173 -
1174 - #[test]
1175 - fn a_figures_change_rides_on_the_value_row() {
1176 - // The delta is the toned part and the value is an ordinary fact, so the
1177 - // two share a row rather than the caption growing a second sentence.
1178 - let style = style();
1179 - let figure_ = Figure::new("42", "open tasks")
1180 - .change("+3")
1181 - .tone(Tone::Success);
1182 - let mut buf = buffer(20, 4);
1183 - figure(&style, &figure_, buf.area, &mut buf);
1184 - assert_eq!(rows(&buf)[0], "42 +3");
1185 - }
1186 -
1187 - #[test]
1188 - fn a_compulsory_field_says_so_in_its_label() {
1189 - let style = style();
1190 - let mut field_ = Field::new(FieldKind::Text, "email", "Email");
1191 - field_.required = true;
1192 - let mut buf = buffer(20, 4);
1193 - field(&style, &field_, Held::Absent, false, buf.area, &mut buf);
1194 - assert_eq!(rows(&buf)[0], "Email *");
1195 - }
1196 -
1197 - #[test]
1198 - fn a_hidden_field_costs_no_rows_at_all() {
1199 - // The one field kind a terminal and a webview agree on completely.
1200 - let style = style();
1201 - let field_ = Field::new(FieldKind::Hidden, "csrf", "Token");
1202 - let mut buf = buffer(20, 4);
1203 - assert_eq!(
1204 - field(
1205 - &style,
1206 - &field_,
1207 - Held::Text("abc"),
1208 - false,
1209 - buf.area,
1210 - &mut buf
1211 - ),
1212 - 0
1213 - );
1214 - assert_eq!(field_height(&style, &field_, 20), 0);
1215 - assert_eq!(rows(&buf)[0], "");
1216 - }
1217 -
1218 - #[test]
1219 - fn a_secret_is_dotted_from_the_callers_buffer_and_never_from_the_description() {
1220 - // The one control that would be undrawable without `held`: a password
1221 - // that came back down the wire is a password in a page and in a log.
1222 - let style = style();
1223 - let field_ = Field::new(FieldKind::Secret, "password", "Password");
1224 - let mut buf = buffer(20, 4);
1225 - field(
1226 - &style,
1227 - &field_,
1228 - Held::Text("hunter2"),
1229 - false,
1230 - buf.area,
1231 - &mut buf,
1232 - );
1233 - assert_eq!(rows(&buf)[1], "*******");
1234 - }
1235 -
1236 - #[test]
1237 - fn an_error_takes_the_row_the_hint_would_have_had() {
1238 - // Once something has gone wrong that is the sentence worth the row,
1239 - // which is the order a webview uses too.
1240 - let style = style();
1241 - let mut field_ = Field::new(FieldKind::Text, "email", "Email");
1242 - field_.hint = Some("work address");
1243 - field_.error = Some("not an address");
1244 - let mut buf = buffer(20, 5);
1245 - field(
1246 - &style,
1247 - &field_,
1248 - Held::Text("nope"),
1249 - false,
1250 - buf.area,
1251 - &mut buf,
1252 - );
1253 - assert_eq!(rows(&buf)[2], "not an address");
1254 - assert_eq!(field_height(&style, &field_, 20), 3);
1255 - }
1256 -
1257 - #[test]
1258 - fn a_focused_empty_box_shows_where_the_typing_will_land() {
1259 - // An empty field under a style is an empty field. Without the caret a
1260 - // focused box with no placeholder drew literally nothing.
1261 - let style = style();
1262 - let field_ = Field::new(FieldKind::Text, "email", "Email");
1263 - let mut buf = buffer(20, 4);
1264 - field(&style, &field_, Held::Absent, true, buf.area, &mut buf);
1265 - let caret = buf.cell((0, 1)).expect("the well's first cell").style();
1266 - assert!(caret.add_modifier.contains(Modifier::REVERSED));
1267 - }
1268 -
1269 - #[test]
1270 - fn a_choice_field_marks_the_chosen_option_and_costs_a_row_each() {
1271 - let style = style();
1272 - let mut field_ = Field::new(FieldKind::Radio, "size", "Size");
1273 - let options = [Choice::plain("small"), Choice::plain("large")];
1274 - field_.options = &options;
1275 - let mut buf = buffer(20, 5);
1276 - field(
1277 - &style,
1278 - &field_,
1279 - Held::Text("large"),
1280 - false,
1281 - buf.area,
1282 - &mut buf,
1283 - );
1284 - assert_eq!(rows(&buf)[1], "( ) small");
1285 - assert_eq!(rows(&buf)[2], "(*) large");
1286 - assert_eq!(field_height(&style, &field_, 20), 3);
1287 - }
1288 -
1289 - #[test]
1290 - fn a_range_draws_its_two_ends_and_where_the_value_sits_between_them() {
1291 - let style = style();
1292 - let field_ = Field::range("review", "Review above", "0", "1");
1293 - let mut buf = buffer(40, 3);
1294 - field(
1295 - &style,
1296 - &field_,
1297 - Held::Text("0.5"),
1298 - false,
1299 - buf.area,
1300 - &mut buf,
1301 - );
1302 - // Ten cells by default, half of them filled, with the extent read out
1303 - // at either side: 0.5 means nothing without the 0 and the 1.
1304 - assert_eq!(rows(&buf)[1].trim_end(), "0 #####----- 1 0.5");
1305 - assert_eq!(field_height(&style, &field_, 40), 2);
1306 - }
1307 -
1308 - #[test]
1309 - fn a_unit_rides_on_the_value_and_not_on_the_label() {
1310 - // The label is a line above; the number is the line the eye is on.
1311 - let style = style();
1312 - let field_ = Field {
1313 - unit: Some("s"),
1314 - ..Field::range("attack", "Attack", "0", "5")
1315 - };
1316 - let mut buf = buffer(40, 3);
1317 - field(
1318 - &style,
1319 - &field_,
1320 - Held::Text("2.5"),
1321 - false,
1322 - buf.area,
1323 - &mut buf,
1324 - );
1325 - assert_eq!(rows(&buf)[0].trim_end(), "Attack");
1326 - assert_eq!(rows(&buf)[1].trim_end(), "0 #####----- 5 2.5 s");
1327 - }
1328 -
1329 - #[test]
1330 - fn a_typed_number_reads_with_its_unit_too() {
1331 - let style = style();
1332 - let field_ = Field {
1333 - unit: Some("ms"),
1334 - ..Field::new(FieldKind::Number, "fade", "Fade")
1335 - };
1336 - let mut buf = buffer(40, 3);
1337 - field(&style, &field_, Held::Text("50"), false, buf.area, &mut buf);
1338 - assert_eq!(rows(&buf)[1].trim_end(), "50 ms");
1339 - }
1340 -
1341 - #[test]
1342 - fn a_unit_on_a_kind_that_is_not_a_quantity_is_ignored() {
1343 - // Which kinds are quantities is the description's answer, not a
1344 - // `matches!` kept in this crate.
1345 - let style = style();
1346 - let field_ = Field {
1347 - unit: Some("s"),
1348 - ..Field::new(FieldKind::Text, "name", "Name")
1349 - };
1350 - let mut buf = buffer(40, 3);
1351 - field(
1352 - &style,
1353 - &field_,
1354 - Held::Text("kick"),
1355 - false,
1356 - buf.area,
1357 - &mut buf,
1358 - );
1359 - assert_eq!(rows(&buf)[1].trim_end(), "kick");
1360 - }
1361 -
1362 - #[test]
1363 - fn an_interval_is_one_line_with_both_ends_on_it() {
1364 - // One question, one line. Two rows would read as two questions, which
1365 - // is the reading the kind exists to prevent.
1366 - let style = style();
1367 - let field_ = Field {
1368 - min: Some("0"),
1369 - max: Some("300"),
1370 - unit: Some("BPM"),
1371 - ..Field::interval("bpm_min", "bpm_max", "BPM range")
1372 - };
1373 - let mut buf = buffer(40, 3);
1374 - field(
1375 - &style,
1376 - &field_,
1377 - Held::Between {
1378 - lower: "90",
1379 - upper: "130",
1380 - },
1381 - false,
1382 - buf.area,
1383 - &mut buf,
1384 - );
1385 - assert_eq!(rows(&buf)[0].trim_end(), "BPM range");
1386 - assert_eq!(rows(&buf)[1].trim_end(), "90 BPM to 130 BPM");
1387 - assert_eq!(rows(&buf)[2].trim_end(), "");
1388 - }
1389 -
1390 - #[test]
1391 - fn an_open_end_falls_back_to_the_bound_it_means() {
1392 - // "Over 120" is an answer rather than a half-filled box, and where the
1393 - // axis ends is what the empty end stands for.
1394 - let style = style();
1395 - let field_ = Field {
1396 - min: Some("0"),
1397 - max: Some("300"),
1398 - ..Field::interval("bpm_min", "bpm_max", "BPM range")
1399 - };
1400 - let mut buf = buffer(40, 3);
1401 - field(
1402 - &style,
1403 - &field_,
1404 - Held::Between {
1405 - lower: "120",
1406 - upper: "",
1407 - },
1408 - false,
1409 - buf.area,
1410 - &mut buf,
1411 - );
1412 - assert_eq!(rows(&buf)[1].trim_end(), "120 to 300");
1413 - }
1414 -
1415 - #[test]
1416 - fn an_unbounded_open_end_draws_nothing_rather_than_a_number() {
1417 - // A terminal inventing a bound here would report a filter nobody
1418 - // applied, which is `range_line`'s position on an unreadable value.
1419 - // What is left reads as the sentence it is: up to 130.
1420 - let style = style();
1421 - let field_ = Field::interval("bpm_min", "bpm_max", "BPM range");
1422 - let mut buf = buffer(40, 3);
1423 - field(
1424 - &style,
1425 - &field_,
1426 - Held::Between {
1427 - lower: "",
1428 - upper: "130",
1429 - },
1430 - false,
1431 - buf.area,
1432 - &mut buf,
1433 - );
1434 - assert_eq!(rows(&buf)[1].trim_end(), "to 130");
1435 - }
1436 -
1437 - #[test]
1438 - fn a_range_holding_something_unreadable_still_shows_it() {
1439 - // The app put the value there. A terminal that quietly rounded it to a
1440 - // bound would be reporting a value nobody set, which is `empty_well`'s
1441 - // position on the same problem.
1442 - let style = style();
1443 - let field_ = Field::range("review", "Review above", "0", "1");
1444 - let mut buf = buffer(40, 3);
1445 - field(
1446 - &style,
1447 - &field_,
1448 - Held::Text("unset"),
1449 - false,
1450 - buf.area,
1451 - &mut buf,
1452 - );
1453 - assert_eq!(rows(&buf)[1].trim_end(), "0 ---------- 1 unset");
1454 - }
1455 -
1456 - #[test]
1457 - fn an_unbounded_range_is_typed_into_rather_than_dragged() {
1458 - // Bounds this crate invented are bounds the user would then drag
1459 - // against. The text path takes every answer the bar would.
1460 - let style = style();
1461 - let field_ = Field {
1462 - max: Some("1"),
1463 - ..Field::new(FieldKind::Range, "review", "Review above")
1464 - };
1465 - let mut buf = buffer(40, 3);
1466 - field(
1467 - &style,
1468 - &field_,
1469 - Held::Text("0.5"),
1470 - false,
1471 - buf.area,
1472 - &mut buf,
1473 - );
1474 - assert_eq!(rows(&buf)[1].trim_end(), "0.5");
1475 - }
1476 -
1477 - #[test]
1478 - fn an_unavailable_option_reads_as_inert_and_says_why() {
1479 - // The one place muted is the truth rather than the lie the convention
1480 - // warns about: this option will not answer, and the reason is on the
1481 - // row rather than nowhere.
1482 - let style = style();
1483 - let options = [
1484 - Choice::new("chromatic", "Chromatic"),
1485 - Choice::new("multi", "Multi-sample").unless("Drop a second sample."),
1486 - ];
1487 - let mut field_ = Field::new(FieldKind::Radio, "mode", "Mode");
1488 - field_.options = &options;
1489 - let mut buf = buffer(46, 4);
Lines truncated
@@ -1,0 +1,747 @@
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_,
Lines truncated