Skip to main content

max / makeover-layout

75.6 KB · 2059 lines History Blame Raw
1 //! The crate's own tests, in the file the crate's `mod tests;` names.
2 //!
3 //! Moved out of `lib.rs` whole: the root is a facade of `pub use` lines, and
4 //! 2,000 lines of tests under 250 lines of re-exports made the facade the
5 //! smaller half of the file that declares it.
6
7 use super::{Change, Intent as _, Syntax};
8
9 /// Every name a caller is told to use, reached through the root path.
10 ///
11 /// The thirteen modules are private, so a member left out of the facade
12 /// stays reachable inside the crate and vanishes from the API outside it,
13 /// which is a silent semver break. This is where that fails instead.
14 #[test]
15 fn the_names_a_caller_is_told_to_use_are_exported_from_the_root() {
16 #[allow(unused_imports)]
17 use crate::{
18 Accepted, Act, Arrangement, Awaiting, Bevel, Candidate, CellPart, Change, Choice, Column,
19 Contrast, Curve, DATE_FORMAT, DATETIME_FORMAT, Depth, Edge, Extent, Facet, FacetValue,
20 Fallback, Family, Field, FieldKind, Figure, Fill, Fit, Flow, Heading, Intent, Loading,
21 Measure, Meter, Nesting, Notice, Paging, Placement, Priority, Readiness, Region, RowPart,
22 Selecting, Selector, Share, Showing, Sort, Span, Standing, State, Syntax, ThemeChoice,
23 ThemeVariant, Token, Tone, Track, Unit, Width, Window,
24 };
25 }
26
27 /// A run nothing claimed is a real answer rather than a missing one, which
28 /// is why this is not an `Option<Syntax>`: a lexer that ran and found
29 /// ordinary code has said something.
30 #[test]
31 fn an_unclassified_run_is_plain_and_plain_is_the_default() {
32 assert_eq!(Syntax::default(), Syntax::Plain);
33 assert_eq!(Syntax::Plain.name(), "plain");
34 }
35
36 /// Every class has a stable lowercase name, and no two share one. The
37 /// spelling is named here rather than agreed per renderer, which is what
38 /// stops one calling it `str` and the next `string`.
39 #[test]
40 fn every_syntax_class_has_its_own_spelling() {
41 let all = [
42 Syntax::Plain,
43 Syntax::Keyword,
44 Syntax::String,
45 Syntax::Comment,
46 Syntax::Constant,
47 Syntax::Entity,
48 Syntax::Variable,
49 Syntax::Support,
50 ];
51 let mut seen = std::collections::BTreeSet::new();
52 for class in all {
53 assert!(!class.name().is_empty(), "{class:?} has no spelling");
54 assert!(seen.insert(class.name()), "{class:?} shares a spelling");
55 }
56 assert_eq!(seen.len(), all.len());
57 }
58
59 /// Context is a state and not an absence, so an unchanged line is
60 /// distinguishable from a line nobody marked. `Syntax::Plain`'s argument.
61 #[test]
62 fn an_unmarked_diff_line_is_context() {
63 assert_eq!(Change::default(), Change::Context);
64 assert_eq!(Change::Context.token(), "content");
65 }
66
67 /// The two sides reuse the status axis rather than minting a palette. An
68 /// app holding them fixed across themes says so in its own sheet.
69 #[test]
70 fn the_two_sides_of_a_change_take_the_status_intents() {
71 assert_eq!(Change::Added.token(), "success");
72 assert_eq!(Change::Removed.token(), "danger");
73 }
74
75 /// The concept is named here and its magnitude is not, which is
76 /// `Awaiting`'s split and is why a renderer can disagree with another
77 /// about what a level is worth without either of them being wrong.
78 #[test]
79 fn nesting_says_how_deep_and_not_how_wide() {
80 assert_eq!(Nesting::default(), Nesting::top());
81 assert_eq!(Nesting::top().level, 0);
82 assert!(!Nesting::top().is_nested());
83
84 let under = Nesting::at(2);
85 assert_eq!(under.level, 2);
86 assert!(under.is_nested());
87
88 // Ordered, so a renderer walking a flat list of rows can compare two
89 // levels rather than reaching into the field.
90 assert!(Nesting::top() < under);
91 }
92
93 use super::*;
94
95 #[test]
96 fn a_markdown_field_is_multiline_and_offers_nothing() {
97 // The editing counterpart of markdown prose is still text: every host
98 // can draw it, which is the whole reason the kind was addable.
99 assert!(FieldKind::Rich.multiline());
100 assert!(FieldKind::Textarea.multiline());
101 assert!(!FieldKind::Text.multiline());
102 // It is not a chooser and not a moment.
103 assert!(!FieldKind::Rich.offers_options());
104 assert!(!FieldKind::Rich.temporal());
105 assert!(FieldKind::Rich.visible());
106 }
107
108 #[test]
109 fn only_the_numeric_kinds_are_measurable() {
110 assert!(FieldKind::Number.measurable());
111 assert!(FieldKind::Range.measurable());
112 // An axis is measured in something and both its ends are in it, so the
113 // unit is read once for the pair rather than per end.
114 assert!(FieldKind::Interval.measurable());
115 // A date is ordered and is not a quantity with a unit to choose: its
116 // unit is fixed by the kind, so saying one would restate `kind`.
117 for kind in [
118 FieldKind::Text,
119 FieldKind::Date,
120 FieldKind::DateTime,
121 FieldKind::Select,
122 FieldKind::Checkbox,
123 FieldKind::File,
124 ] {
125 assert!(!kind.measurable(), "{kind:?}");
126 }
127 }
128
129 #[test]
130 fn a_field_carries_no_unit_until_one_is_given() {
131 // Additive: absent is what every field described before 0.33.0 meant.
132 let plain = Field::new(FieldKind::Number, "attack", "Attack");
133 assert_eq!(plain.unit, None);
134 let measured = Field {
135 unit: Some("s"),
136 ..Field::range("attack", "Attack", "0.001", "5")
137 };
138 assert_eq!(measured.unit, Some("s"));
139 assert!(measured.kind.measurable());
140 }
141
142 #[test]
143 fn an_interval_states_both_ends_names() {
144 // Stated rather than derived: the two measured sites disagree about
145 // affix order, so a rule here would rename one of them.
146 let suffixed = Field::interval("bpm_min", "bpm_max", "BPM");
147 assert_eq!(suffixed.name, "bpm_min");
148 assert_eq!(suffixed.upper_name, Some("bpm_max"));
149 let prefixed = Field::interval("min_price", "max_price", "Price");
150 assert_eq!(prefixed.name, "min_price");
151 assert_eq!(prefixed.upper_name, Some("max_price"));
152 assert_eq!(suffixed.kind, FieldKind::Interval);
153 }
154
155 #[test]
156 fn every_other_kind_has_no_upper_end() {
157 // Additive: absent is what every field described before 0.34.0 meant.
158 for kind in [FieldKind::Text, FieldKind::Number, FieldKind::Range] {
159 assert_eq!(Field::new(kind, "n", "N").upper_name, None, "{kind:?}");
160 }
161 assert_eq!(Field::range("t", "T", "0", "1").upper_name, None);
162 }
163
164 #[test]
165 fn an_interval_owes_no_bounds_and_takes_the_axis_facts_once() {
166 // A range's bounds are the control's extent and are owed; an interval's
167 // are a rule on each end, which is Number's arrangement.
168 let plain = Field::interval("bpm_min", "bpm_max", "BPM");
169 assert!(!plain.bounded());
170 let axis = Field {
171 min: Some("0"),
172 max: Some("300"),
173 step: Some("1"),
174 unit: Some("BPM"),
175 ..Field::interval("bpm_min", "bpm_max", "BPM")
176 };
177 assert!(axis.bounded());
178 assert_eq!(axis.unit, Some("BPM"));
179 // Nothing here checks the crossing rule, exactly as nothing checks
180 // `min` for a number: the description carries constraints and whoever
181 // validated decides a value is wrong.
182 assert!(axis.error.is_none());
183 }
184
185 #[test]
186 fn only_a_subtree_prunes_and_only_the_listing_modes_offer_values() {
187 // Excluding a value from a flat facet is the same fact as not picking
188 // it, so the affordance exists in exactly one mode.
189 assert!(Selecting::Subtree.prunes());
190 for mode in [
191 Selecting::OneOf,
192 Selecting::AnyOf,
193 Selecting::Range,
194 Selecting::Text,
195 ] {
196 assert!(!mode.prunes(), "{mode:?}");
197 }
198 // Text and Range answer with something that is not one of a set.
199 assert!(!Selecting::Text.offers_values());
200 assert!(!Selecting::Range.offers_values());
201 assert!(Selecting::OneOf.offers_values());
202 assert!(Selecting::AnyOf.accumulates());
203 assert!(!Selecting::OneOf.accumulates());
204 }
205
206 #[test]
207 fn an_inherited_value_is_in_force_without_having_been_picked() {
208 // The distinction a bool cannot hold, and the reason Standing has four
209 // members: a child under a taken parent narrows the set, and clearing
210 // it clears nothing.
211 assert!(Standing::Inherited.in_force());
212 assert!(!Standing::Inherited.is_picked());
213 assert!(Standing::Taken.in_force());
214 assert!(Standing::Taken.is_picked());
215 // A prune is a decision that takes the value out.
216 assert!(Standing::Pruned.is_picked());
217 assert!(!Standing::Pruned.in_force());
218 assert!(!Standing::Open.is_picked());
219 assert!(!Standing::Open.in_force());
220 // A pruned branch still answers a press, so it may not read as inert.
221 assert_ne!(Standing::Pruned.intent(), "content-muted");
222 }
223
224 #[test]
225 fn a_facet_is_engaged_by_a_decision_and_not_by_an_inherited_value() {
226 let inherited = [
227 FacetValue::of("music")
228 .standing(Standing::Taken)
229 .at(Nesting::at(0), true),
230 FacetValue::new("music/synths", "synths")
231 .standing(Standing::Inherited)
232 .at(Nesting::at(1), false),
233 ];
234 let facet = Facet::new("Tag", Selecting::Subtree, &inherited);
235 assert!(facet.engaged());
236 // The gutter an indenting renderer reserves before its first paint.
237 assert_eq!(facet.reach(), 1);
238
239 let untouched = [
240 FacetValue::of("music").at(Nesting::at(0), true),
241 FacetValue::new("music/synths", "synths")
242 .standing(Standing::Inherited)
243 .at(Nesting::at(1), false),
244 ];
245 // Inherited alone is something further up doing the narrowing, and
246 // there is nothing further up here.
247 assert!(!Facet::new("Tag", Selecting::Subtree, &untouched).engaged());
248
249 // A text facet lists nothing, so it is flat and never reads as engaged
250 // from its values: the typed string is not held here.
251 let typed = Facet::new("Search", Selecting::Text, &[]);
252 assert!(!typed.engaged());
253 assert_eq!(typed.reach(), 0);
254 }
255
256 #[test]
257 fn a_count_is_absent_rather_than_zero_when_it_was_not_measured() {
258 // Awaiting::amount's rule in a second place: a written zero reads as
259 // "none of them", which is a different claim from "not counted".
260 assert_eq!(FacetValue::of("Ambient").count, None);
261 assert_eq!(FacetValue::of("Ambient").counted(0).count, Some(0));
262 }
263
264 #[test]
265 fn one_kind_takes_files_and_the_two_file_members_are_its_alone() {
266 assert!(FieldKind::File.takes_files());
267 for kind in [
268 FieldKind::Text,
269 FieldKind::Textarea,
270 FieldKind::Rich,
271 FieldKind::Select,
272 FieldKind::Checkbox,
273 FieldKind::Hidden,
274 ] {
275 assert!(!kind.takes_files());
276 }
277 // The default is a field that takes any one file, which is what an
278 // input with no accept and no multiple already is.
279 let plain = Field::new(FieldKind::File, "cover", "Cover");
280 assert!(plain.accept.is_empty());
281 assert!(!plain.multiple);
282 }
283
284 #[test]
285 fn an_accept_list_says_which_disclosure_and_a_suffix_says_none() {
286 // The three shapes are the MNW server's own three, and the family is
287 // the question a renderer asks before it keeps room for a preview.
288 assert_eq!(
289 Accepted::Family(Family::Image).family(),
290 Some(Family::Image)
291 );
292 assert_eq!(Accepted::Type("image/jpeg").family(), Some(Family::Image));
293 assert_eq!(Accepted::Type("audio/flac").family(), Some(Family::Audio));
294 assert_eq!(
295 Accepted::Type("video/quicktime").family(),
296 Some(Family::Video)
297 );
298 // A media type outside the three families names none, and neither does
299 // a suffix. `.mp3` is audio in fact and this crate will not infer it:
300 // the table that said so would rot.
301 assert_eq!(Accepted::Type("text/csv").family(), None);
302 assert_eq!(Accepted::Suffix(".mp3").family(), None);
303 assert_eq!(Accepted::Suffix(".tar.gz").family(), None);
304 // Media types are case-insensitive and half the tree writes them
305 // lowercase by habit rather than by rule.
306 assert_eq!(Accepted::Type("IMAGE/PNG").family(), Some(Family::Image));
307 }
308
309 #[test]
310 fn every_accepted_entry_has_one_spelling_a_host_can_write() {
311 assert_eq!(Accepted::Family(Family::Image).as_str(), "image/*");
312 assert_eq!(Accepted::Family(Family::Audio).as_str(), "audio/*");
313 assert_eq!(Accepted::Family(Family::Video).as_str(), "video/*");
314 assert_eq!(Accepted::Type("text/csv").as_str(), "text/csv");
315 assert_eq!(Accepted::Suffix(".tar.gz").as_str(), ".tar.gz");
316 }
317
318 #[test]
319 fn a_list_accepting_two_families_still_has_a_disclosure_to_offer() {
320 // The measured dropzone: `accept="image/*,video/*"`. There is no single
321 // family to return and there is still a preview to keep room for, which
322 // is why the question is asked of the list rather than of one entry.
323 const MEDIA: &[Accepted<'_>] = &[
324 Accepted::Family(Family::Image),
325 Accepted::Family(Family::Video),
326 ];
327 assert!(Field::upload("media", "Media", MEDIA).accepts_media());
328 // An installer's suffix list wants no disclosure, which is the measured
329 // case rather than a hypothetical one.
330 const BUILDS: &[Accepted<'_>] = &[Accepted::Suffix(".zip"), Accepted::Suffix(".dmg")];
331 assert!(!Field::upload("build", "Build", BUILDS).accepts_media());
332 // And a field that takes anything says so by listing nothing.
333 assert!(!Field::upload("any", "File", &[]).accepts_media());
334 }
335
336 #[test]
337 fn an_upload_carries_its_list_and_takes_one_file_until_it_says_otherwise() {
338 const IMAGES: &[Accepted<'_>] = &[
339 Accepted::Type("image/jpeg"),
340 Accepted::Type("image/png"),
341 Accepted::Type("image/webp"),
342 ];
343 let avatar = Field::upload("avatar", "Avatar", IMAGES);
344 assert_eq!(avatar.kind, FieldKind::File);
345 assert_eq!(avatar.accept, IMAGES);
346 assert!(!avatar.multiple);
347 let several = Field {
348 multiple: true,
349 ..avatar
350 };
351 assert!(several.multiple);
352 }
353
354 #[test]
355 fn the_four_readiness_states_are_one_axis_and_only_one_shows_content() {
356 // Mutually exclusive is the test for one enum against several fields: a
357 // region shows its content, or that it is coming, or that there is none,
358 // or that it broke. Never two.
359 assert!(Readiness::Ready.shows_content());
360 for state in [Readiness::Pending, Readiness::Empty, Readiness::Failed] {
361 assert!(!state.shows_content());
362 }
363 }
364
365 #[test]
366 fn a_region_shows_all_of_its_children_unless_it_says_otherwise() {
367 // The default is the behaviour every region had before this member
368 // existed, which is what keeps it additive: a description written
369 // against 0.22.0 says the same thing under 0.23.0.
370 assert_eq!(Showing::default(), Showing::All);
371 assert!(!Showing::All.selective());
372 }
373
374 #[test]
375 fn only_a_disclosure_can_show_nothing() {
376 // The two derived idioms differ in one respect and this is it. A
377 // carousel's row moves between frames and never reaches empty; a
378 // disclosure's summary line is the same control wearing its closed
379 // state, so a renderer has to know which it is drawing.
380 assert!(Showing::AtMostOne.dismissible());
381 assert!(!Showing::One.dismissible());
382 assert!(!Showing::All.dismissible());
383
384 // Both are selective, though. Deriving chrome is one question and
385 // whether that chrome closes is another.
386 assert!(Showing::One.selective());
387 assert!(Showing::AtMostOne.selective());
388 }
389
390 #[test]
391 fn an_empty_region_is_not_a_broken_one() {
392 // An empty list is the normal state of a new install. Drawing it in a
393 // danger tone reports a fault where there is none, and this is the one
394 // place the distinction is carried.
395 assert_eq!(Readiness::Empty.tone(), Tone::Neutral);
396 assert_eq!(Readiness::Failed.tone(), Tone::Danger);
397 assert_eq!(Readiness::Pending.tone(), Tone::Neutral);
398 }
399
400 #[test]
401 fn a_column_can_be_sorted_without_being_sortable() {
402 // Both combinations mean something, which is why the two fields are
403 // independent rather than one implying the other. A list ordered by a
404 // key the user cannot change is a real thing with a caret worth drawing.
405 let fixed = Column {
406 sorted: Some(Sort::Descending),
407 ..Column::new("Created")
408 };
409
410 assert!(!fixed.sortable);
411 assert_eq!(fixed.sorted.map(Sort::as_str), Some("descending"));
412
413 let offered = Column {
414 sortable: true,
415 ..Column::new("Name")
416 };
417 assert_eq!(offered.sorted, None);
418 }
419
420 #[test]
421 fn a_direction_flips_and_says_what_it_is() {
422 assert_eq!(Sort::Ascending.reversed(), Sort::Descending);
423 assert_eq!(Sort::Descending.reversed().reversed(), Sort::Descending);
424 assert_eq!(Sort::Ascending.as_str(), "ascending");
425 }
426
427 #[test]
428 fn a_direction_carries_its_caret_and_the_two_are_not_the_same_glyph() {
429 // The spelling every renderer reads, so that agreeing is composition
430 // rather than three files happening to hold the same literal.
431 assert_eq!(Sort::Ascending.glyph(), "\u{25B2}");
432 assert_eq!(Sort::Descending.glyph(), "\u{25BC}");
433 assert_ne!(Sort::Ascending.glyph(), Sort::Descending.glyph());
434 // Bare. The gap is the renderer's, and a space here would be a second
435 // one wherever a renderer already carries its own.
436 for d in [Sort::Ascending, Sort::Descending] {
437 assert_eq!(d.glyph().trim(), d.glyph());
438 }
439 }
440
441 #[test]
442 fn a_figure_carries_its_tone_because_no_renderer_can_derive_it() {
443 // Three of goingson's five sites tone the figure by their own means, so
444 // tone is carried at every site that needs it and derived at none. The
445 // same reasoning `Meter` reached, from a different direction.
446 let streak = Figure::new("0", "Current Streak").tone(Tone::Warning);
447 assert_eq!(streak.tone, Tone::Warning);
448 assert_eq!(Figure::new("17", "Total").tone, Tone::Neutral);
449 }
450
451 #[test]
452 fn a_figures_change_is_the_toned_part_and_is_absent_by_default() {
453 // 0.13.0. The MNW server's stat card is a label, a value and a delta,
454 // across four screens, and the delta is what reads as good or bad. Tone
455 // had no consumer before this: the figure itself is an ordinary fact.
456 let views = Figure::new("1,204", "Views")
457 .change("+12.5%")
458 .tone(Tone::Success);
459 assert_eq!(views.change, Some("+12.5%"));
460 assert_eq!(views.tone, Tone::Success);
461
462 // A figure with nothing to compare against says so by having no change,
463 // rather than by carrying an empty string a renderer has to test for.
464 assert_eq!(Figure::new("3.1%", "Conversion").change, None);
465 }
466
467 #[test]
468 fn a_figures_value_is_text_because_only_the_app_knows_what_it_is() {
469 // "84%", "12/30", "3d". A figure is whatever the app computed, already
470 // formatted, and that is the line between this and `Meter`: a meter is
471 // a proportion a renderer draws, a figure is a fact it sets in type.
472 for value in ["84%", "12/30", "3d"] {
473 assert_eq!(Figure::new(value, "Rate").value, value);
474 }
475 }
476
477 #[test]
478 fn a_proportion_is_a_row_part_and_takes_no_intent_of_its_own() {
479 // The meter carries the tone, so a part-level intent underneath would
480 // fight it. Same answer `Tokens` needed, for the same reason.
481 assert_eq!(RowPart::Proportion.intent(), RowPart::Tokens.intent());
482 }
483
484 #[test]
485 fn a_file_field_is_drawn_and_offers_no_options() {
486 // It is a control the user operates, unlike `Hidden`, and it does not
487 // pick from a list the description carries, unlike `Select`.
488 assert!(FieldKind::File.visible());
489 assert!(!FieldKind::File.offers_options());
490 assert!(!FieldKind::File.confidential());
491 }
492
493 #[test]
494 fn a_constraint_is_a_fact_about_the_question_and_not_a_verdict() {
495 // The whole model: the description carries the rule, the renderer emits
496 // its host's idiom, and `error` is what arrives back when someone
497 // validated. Nothing here decides a value is wrong.
498 let field = Field {
499 max_length: Some(100),
500 min: Some("1"),
501 max: Some("240"),
502 required: true,
503 ..Field::new(FieldKind::Number, "minutes", "Minutes")
504 };
505 assert!(!field.invalid());
506
507 // A bound is text because it is only a number for some of the kinds
508 // that take one. goingson has both shapes live.
509 let when = Field {
510 min: Some("2026-08-09T14:30"),
511 ..Field::new(FieldKind::Text, "starts", "Starts")
512 };
513 assert_eq!(when.min, Some("2026-08-09T14:30"));
514 }
515
516 #[test]
517 fn a_meter_keeps_the_over_run_the_percentage_throws_away() {
518 // The whole reason this is a pair. goingson's `Task::time_progress`
519 // clamps to 100 and then carries `is_over_estimate` beside it to say
520 // what the clamp dropped; a meter says both from one fact.
521 let over = Meter::new(45, 30);
522 assert_eq!(over.percent(), 100);
523 assert!(over.overflowing());
524
525 let exact = Meter::new(30, 30);
526 assert_eq!(exact.percent(), over.percent());
527 assert!(!exact.overflowing());
528 }
529
530 #[test]
531 fn an_empty_set_does_not_divide_by_zero() {
532 // Sayable on purpose, so it has to be answerable. A meter over an
533 // unloaded count is what an app actually has for a frame.
534 let none = Meter::new(0, 0);
535 assert_eq!(none.percent(), 0);
536 assert!(none.is_empty());
537 assert!(!none.overflowing());
538 }
539
540 #[test]
541 fn the_ratio_survives_where_a_percentage_would_not() {
542 // Given 43 nothing can recover "3 of 7", which is why the numbers are
543 // carried and the label names only the noun.
544 let m = Meter::new(3, 7).label("subtasks");
545 assert_eq!(m.percent(), 42);
546 assert_eq!((m.done, m.total), (3, 7));
547 assert_eq!(m.label, Some("subtasks"));
548 }
549
550 #[test]
551 fn tone_is_carried_because_no_renderer_can_derive_it() {
552 // The same fullness means opposite things on two of goingson's bars,
553 // and only the app knows which.
554 let subtasks = Meter::new(9, 10).tone(Tone::Success);
555 let estimate = Meter::new(9, 10).tone(Tone::Danger);
556 assert_eq!(subtasks.percent(), estimate.percent());
557 assert_ne!(subtasks.tone, estimate.tone);
558 // Untoned by default: a bar says nothing about status until something
559 // says so, the same way a row is not selectable until told.
560 assert_eq!(Meter::new(9, 10).tone, Tone::Neutral);
561 }
562
563 #[test]
564 fn an_act_is_reachable_until_it_is_disabled() {
565 // The one member a renderer must branch on, and since 0.19.0 the only
566 // member there is. A stated state is not by itself a reason to stop
567 // answering, which is the distinction `State` makes and every
568 // hand-rolled button in the tree had to remember.
569 assert!(!Act::new("Save").disabled());
570 assert!(Act::new("Save").state(State::Disabled).disabled());
571 }
572
573 #[test]
574 fn an_act_carries_its_key_because_a_terminal_has_nothing_else() {
575 // No key is the ordinary case, and the webview hosts that ignore it
576 // are why it stayed optional.
577 assert_eq!(Act::new("Delete").key, None);
578 let quit = Act::new("Quit").key("q").tone(Tone::Danger);
579 assert_eq!(quit.key, Some("q"));
580 assert_eq!(quit.tone, Tone::Danger);
581 }
582
583 #[test]
584 fn a_meter_does_not_overflow_on_large_counts() {
585 // done * 100 in u32 would wrap somewhere past 42 million. Counts that
586 // size are not tasks, but a description layer that silently reports 3%
587 // for a full bar is worse than one that is slow.
588 let big = Meter::new(u32::MAX, u32::MAX);
589 assert_eq!(big.percent(), 100);
590 assert!(!big.overflowing());
591 }
592
593 #[test]
594 fn inset_is_raised_with_the_light_moved() {
595 let (rl, rd) = Bevel::Raised.edges();
596 let (il, id) = Bevel::Inset.edges();
597 assert_eq!((rl, rd), (Edge::Light, Edge::Dark));
598 assert_eq!((il, id), (rd, rl));
599 }
600
601 #[test]
602 fn pressing_twice_is_a_no_op() {
603 for b in [Bevel::Raised, Bevel::Inset] {
604 assert_eq!(b.pressed().pressed(), b);
605 }
606 }
607
608 #[test]
609 fn a_raised_region_is_never_filled_with_a_recessed_surface() {
610 // The bug this vocabulary exists to make unrepresentable.
611 assert_eq!(Depth::Raised.fill(), Some(Fill::Raised));
612 assert_eq!(Depth::Raised.bevel(), Some(Bevel::Raised));
613 assert_eq!(Depth::Well.bevel(), Some(Bevel::Inset));
614 assert_ne!(Depth::Well.fill(), Depth::Raised.fill());
615 }
616
617 #[test]
618 fn state_is_orthogonal_to_depth() {
619 // The reason State is its own axis and not a Depth member: a disabled
620 // button and a disabled field are both disabled and are not the same
621 // shape, which one shared variant could not have said.
622 assert_eq!(Depth::Raised.fill(), Some(Fill::Raised));
623 assert_eq!(Depth::Well.fill(), Some(Fill::Well));
624 assert!(State::Disabled.suppresses_interaction());
625 }
626
627 #[test]
628 fn only_disabled_stops_answering() {
629 // Kept in spirit from the version where `Focus` was the counter-example:
630 // suppressing interaction is `Disabled`'s alone, so a member added here
631 // later does not get to inherit it by being a state.
632 assert!(State::Disabled.suppresses_interaction());
633 }
634
635 #[test]
636 fn disabled_resolves_against_an_intent_makeover_already_derives() {
637 // No new token, so this costs no `makeover` release.
638 assert_eq!(State::Disabled.token(), "content-muted");
639 }
640
641 #[test]
642 fn flat_has_neither_edge_nor_fill() {
643 assert_eq!(Depth::Flat.bevel(), None);
644 assert_eq!(Depth::Flat.fill(), None);
645 }
646
647 #[test]
648 fn sunken_is_recessed_by_colour_with_no_edge() {
649 // The one member carrying a fill without a bevel. A renderer that
650 // assumes the two arrive together drops the fill silently, which is
651 // exactly what makeover-webview did before 0.3.0.
652 assert_eq!(Depth::Sunken.fill(), Some(Fill::Sunken));
653 assert_eq!(Depth::Sunken.bevel(), None);
654 }
655
656 #[test]
657 fn sunken_and_flat_are_different_claims() {
658 // Both edgeless, and only one of them needs a colour. Collapsing them
659 // is what left an unchosen tab unsayable.
660 assert_eq!(Depth::Flat.bevel(), Depth::Sunken.bevel());
661 assert_ne!(Depth::Flat.fill(), Depth::Sunken.fill());
662 }
663
664 #[test]
665 fn a_sunken_surface_is_not_a_well() {
666 // Authored in opposite directions: makeover derives surface-well by
667 // inverting against the theme's content colour, while surface-sunken is
668 // authored and may sit darker than raised.
669 assert_ne!(Fill::Sunken, Fill::Well);
670 assert_eq!(Fill::Sunken.token(), "surface-sunken");
671 assert_eq!(Fill::Well.token(), "surface-well");
672 }
673
674 #[test]
675 fn every_selector_describes_both_of_its_states() {
676 // The gap 0.3.0 closed. Before it, only `chosen` existed and the
677 // unchosen option fell through to Flat at every renderer.
678 for s in [Selector::Tabs, Selector::Segmented, Selector::Toggle] {
679 assert_ne!(
680 s.chosen(),
681 s.unchosen(),
682 "{s:?} cannot tell picked from unpicked"
683 );
684 }
685 }
686
687 #[test]
688 fn only_a_tab_inverts_the_other_way() {
689 // Tabs recede so the chosen one comes forward; a segment and a toggle
690 // stand up so the chosen one is held in. That inversion is the whole
691 // content of "picked" once colour is deferred, and it is why the three
692 // are not one member with a flag.
693 assert_eq!(Selector::Tabs.unchosen(), Depth::Sunken);
694 assert_eq!(Selector::Tabs.chosen(), Depth::Raised);
695
696 for s in [Selector::Segmented, Selector::Toggle] {
697 assert_eq!(s.unchosen(), Depth::Raised);
698 assert_eq!(s.chosen(), Depth::Well);
699 // Held in is what pressing produces: one appearance, two reasons.
700 assert_eq!(s.unchosen().pressed(), s.chosen());
701 }
702 }
703
704 #[test]
705 fn pressing_a_card_makes_a_well() {
706 assert_eq!(Depth::Raised.pressed(), Depth::Well);
707 assert_eq!(
708 Depth::Raised.pressed().bevel(),
709 Depth::Raised.bevel().map(Bevel::pressed)
710 );
711 // Only raised regions respond to being pressed.
712 assert_eq!(Depth::Flat.pressed(), Depth::Flat);
713 assert_eq!(Depth::Well.pressed(), Depth::Well);
714 // An overlay is a surface, not a control.
715 assert_eq!(Depth::Overlay.pressed(), Depth::Overlay);
716 }
717
718 #[test]
719 fn an_overlay_is_lifted_rather_than_edged() {
720 // The wave-2 rule: a surface over the page takes elevation, a surface
721 // in the page takes a bevel. Both halves come off the one Depth, so
722 // they cannot disagree.
723 assert_eq!(Depth::Overlay.fill(), Some(Fill::Overlay));
724 assert_eq!(Depth::Overlay.bevel(), None);
725
726 // Three depths have no bevel and they are not the same claim. Flat has
727 // nothing to separate from, Sunken's colour is doing the separating,
728 // and an overlay is separated by the lift.
729 assert_ne!(Depth::Overlay.fill(), Depth::Sunken.fill());
730 assert_ne!(Depth::Overlay.fill(), Depth::Flat.fill());
731 }
732
733 #[test]
734 fn a_note_is_neither_a_hint_nor_an_error() {
735 // audiofiles' export Format field: choosing WAV over Original
736 // re-encodes and drops the embedded metadata. Perfectly valid, and it
737 // costs something.
738 let format = Field {
739 note: Some((Tone::Warning, "Re-encoding drops embedded BWF and iXML")),
740 ..Field::select("format", "Format", &[])
741 };
742 assert!(!format.invalid(), "a note is not a validation failure");
743 assert!(format.hint.is_none());
744 assert!(format.error.is_none());
745 assert_eq!(format.note.unwrap().0, Tone::Warning);
746
747 // The default is no note, so the 15 in-tree `..Field::new(..)`
748 // literals absorb the member with no call-site edit.
749 assert!(Field::new(FieldKind::Text, "title", "Title").note.is_none());
750 }
751
752 #[test]
753 fn neutral_is_content_not_muted_content() {
754 // Neutral means "no status", and that is all it means. It answered
755 // `content-muted` until 2026-08-27, which muted a figure's headline
756 // number to the colour of its own caption. What makes a badge quiet
757 // is `Token::Badge` answering no click, which lives on the renderer.
758 assert_eq!(Tone::Neutral.token(), "content");
759 assert!(!Token::Badge.interactive());
760 assert_eq!(State::Disabled.token(), "content-muted");
761 }
762
763 #[test]
764 fn intents_name_makeover_tokens_and_nothing_else() {
765 assert_eq!(Edge::Light.token(), "bevel-light");
766 assert_eq!(Edge::Dark.token(), "bevel-dark");
767 assert_eq!(Fill::Raised.token(), "surface-raised");
768 assert_eq!(Fill::Well.token(), "surface-well");
769 // No value ever leaves this crate.
770 for t in [
771 Edge::Light.token(),
772 Edge::Dark.token(),
773 Tone::Danger.token(),
774 Tone::Neutral.token(),
775 State::Disabled.token(),
776 ] {
777 assert!(!t.starts_with('#'), "{t} looks like a value");
778 assert!(
779 !t.chars().next().unwrap().is_ascii_digit(),
780 "{t} is a value"
781 );
782 }
783 }
784
785 #[test]
786 fn a_badge_cannot_be_pressed_and_a_chip_latches() {
787 // The one line that runs through all three apps' taxonomies.
788 assert!(!Token::Badge.interactive());
789 assert!(Token::Chip { removable: false }.interactive());
790 assert!(Token::Chip { removable: true }.interactive());
791
792 // A badge is a label, so giving it an edge would lie about it.
793 assert_eq!(Token::Badge.depth(false), Depth::Flat);
794 assert_eq!(Token::Badge.depth(true), Depth::Flat);
795
796 // A latched chip wears the same shape a pressed one does.
797 let chip = Token::Chip { removable: false };
798 assert_eq!(chip.depth(false), Depth::Raised);
799 assert_eq!(chip.depth(true), Depth::Raised.pressed());
800 }
801
802 #[test]
803 fn a_toast_and_a_banner_differ_in_more_than_placement() {
804 assert!(Notice::Toast.transient());
805 assert!(!Notice::Banner.transient());
806 // A toast floats above the page; a banner rests in the flow.
807 assert_eq!(Notice::Toast.fill(), Fill::Overlay);
808 assert_eq!(Notice::Banner.fill(), Fill::Raised);
809 }
810
811 #[test]
812 fn emphasis_falls_off_down_the_row() {
813 // `revealed_on_hover` was asserted here until 0.13.0 retired it. It said
814 // a row's actions stay hidden until hover, which stopped being true when
815 // makeover-webview 0.23.0 showed them at rest, and nothing had consumed
816 // it for a release either way.
817 assert_eq!(RowPart::Primary.intent(), "content");
818 assert_eq!(RowPart::Secondary.intent(), "content-secondary");
819 assert_eq!(RowPart::Meta.intent(), "content-muted");
820 }
821
822 #[test]
823 fn a_token_part_carries_no_intent_of_its_own() {
824 // Each token carries its own tone, so a part-level intent underneath
825 // would fight the thing sitting on it. Same reasoning as actions, which
826 // is why they answer alike.
827 assert_eq!(RowPart::Tokens.intent(), RowPart::Actions.intent());
828 assert_eq!(RowPart::Tokens.intent(), "content");
829 }
830
831 #[test]
832 fn the_two_temporal_kinds_are_the_two_that_name_a_moment() {
833 // The pair is named once so a host with parsing to do asks here rather
834 // than spelling it out, which is `offers_options`' reason.
835 assert!(FieldKind::Date.temporal());
836 assert!(FieldKind::DateTime.temporal());
837
838 for kind in [
839 FieldKind::Text,
840 FieldKind::Secret,
841 FieldKind::Number,
842 FieldKind::Email,
843 FieldKind::Url,
844 FieldKind::Tel,
845 FieldKind::Range,
846 FieldKind::Textarea,
847 FieldKind::Rich,
848 FieldKind::Select,
849 FieldKind::Radio,
850 FieldKind::Checkbox,
851 FieldKind::File,
852 FieldKind::Hidden,
853 ] {
854 assert!(!kind.temporal(), "{kind:?}");
855 }
856 }
857
858 #[test]
859 fn a_date_carries_no_time_and_a_datetime_carries_no_zone() {
860 // The formats are the whole reason the members are worth naming apart
861 // from text, so the doc comments and the constants have to agree. A
862 // host reading one and meeting the other is the silent failure.
863 assert_eq!(DATE_FORMAT, "%Y-%m-%d");
864 assert!(!DATE_FORMAT.contains("%H"), "a day carries no hour");
865
866 assert_eq!(DATETIME_FORMAT, "%Y-%m-%dT%H:%M");
867 assert!(
868 DATETIME_FORMAT.starts_with(DATE_FORMAT),
869 "a moment starts with the day it is on"
870 );
871 // Local, and that is a property of the value rather than an omission.
872 assert!(!DATETIME_FORMAT.contains("%Z"), "no zone name");
873 assert!(!DATETIME_FORMAT.ends_with('Z'), "not UTC-stamped");
874 assert!(!DATETIME_FORMAT.contains("%S"), "no seconds by default");
875 }
876
877 #[test]
878 fn a_temporal_kind_takes_a_label_above_it_and_offers_no_options() {
879 // Neither is a checkbox and neither is a fixed set, so both fall where
880 // text does. Asserted because a new kind lands in three predicates and
881 // only one of them is the interesting one.
882 for kind in [FieldKind::Date, FieldKind::DateTime] {
883 assert!(kind.visible(), "{kind:?}");
884 assert!(!kind.confidential(), "{kind:?}");
885 assert!(!kind.labels_itself(), "{kind:?}");
886 assert!(!kind.offers_options(), "{kind:?}");
887 }
888 }
889
890 #[test]
891 fn a_cell_part_names_an_intent_and_only_the_value_is_text() {
892 // The table half of what RowPart::intent does for rows. A cell holding
893 // a control and a cell holding text answered alike until 0.14.0, and a
894 // control in a cell took the cell's text colour.
895 assert_eq!(CellPart::Value.intent(), "content");
896
897 for part in [CellPart::Tokens, CellPart::Actions, CellPart::Link] {
898 // Each for its own reason -- a token carries its tone, an action is
899 // a control, a link takes the action colour -- and all three reach
900 // the intent inheriting already gives.
901 assert_eq!(part.intent(), CellPart::Value.intent(), "{part:?}");
902 }
903 }
904
905 #[test]
906 fn every_cell_part_answers_with_a_token_and_never_a_value() {
907 for part in [
908 CellPart::Value,
909 CellPart::Tokens,
910 CellPart::Actions,
911 CellPart::Link,
912 ] {
913 let intent = part.intent();
914 assert!(!intent.is_empty(), "{part:?} names nothing");
915 assert!(!intent.starts_with('#'), "{part:?} looks like a value");
916 }
917 }
918
919 #[test]
920 fn a_separator_is_what_tells_a_section_from_a_subsection() {
921 assert!(Heading::Section.separated());
922 assert!(!Heading::Subsection.separated());
923 assert!(!Heading::Page.separated());
924 }
925
926 #[test]
927 fn a_chosen_segment_is_held_in_and_a_chosen_tab_comes_forward() {
928 assert_eq!(Selector::Segmented.chosen(), Depth::Well);
929 assert_eq!(Selector::Toggle.chosen(), Depth::Well);
930 // The exception, and the whole folder semantic: the open tab joins its
931 // pane rather than sinking away from it.
932 assert_eq!(Selector::Tabs.chosen(), Depth::Raised);
933
934 // A held-in segment is indistinguishable from a pressed raised one,
935 // which is the economy the light model buys over a colour swap.
936 assert_eq!(Selector::Segmented.chosen(), Depth::Raised.pressed());
937
938 // A toggle stands alone; the other two are built out of parts that
939 // touch.
940 assert!(Selector::Segmented.abutting());
941 assert!(Selector::Tabs.abutting());
942 assert!(!Selector::Toggle.abutting());
943 }
944
945 #[test]
946 fn columns_are_peers_and_a_split_is_not() {
947 // The distinction the member exists for. A split's two panes stand in a
948 // master-detail relationship; columns choose nothing about each other.
949 // Both are flat, so depth cannot tell them apart and the doc has to.
950 assert_eq!(Region::Columns.depth(), Depth::Flat);
951 assert_eq!(Region::Split.depth(), Depth::Flat);
952 assert_ne!(Region::Columns, Region::Split);
953 }
954
955 #[test]
956 fn columns_carry_no_count_and_no_share() {
957 // The two things a board is always asked to carry and must not. How
958 // many is what the children say; how wide is settled by "peers are
959 // equal".
960 //
961 // The guard is the binding itself and it is a compile-time one: adding
962 // a field to `Columns` stops this line compiling, which is a better
963 // failure than any assertion about it. Written out rather than inlined
964 // for exactly that reason.
965 let columns: Region<'_> = Region::Columns;
966 assert_eq!(columns.name(), None);
967 }
968
969 #[test]
970 fn columns_are_described_and_the_escape_hatch_is_still_one_member() {
971 // A board's contents are ordinary description all the way down, so a
972 // renderer that does not lay them across still draws every column.
973 // Stacking them vertically is honouring this member, not degrading it.
974 assert!(Region::Columns.described());
975 assert!(!Region::Handover { name: "timeline" }.described());
976 }
977
978 #[test]
979 fn a_span_never_has_zero_minutes_however_it_is_asked_for() {
980 // Every renderer divides by this. A caller passing a backwards or empty
981 // span is a bug, but it is not a bug worth a panic three renderers deep.
982 assert_eq!(Span::new(600, 600).length(), 1);
983 assert_eq!(Span::new(600, 300).length(), 1);
984 assert_eq!(Span::DAY.length(), 1440);
985 }
986
987 #[test]
988 fn a_span_can_run_past_midnight_without_a_second_date() {
989 // 22:00 to 02:00. The alternative was carrying a date, which drags a
990 // timezone into the vocabulary for the sake of one night shift.
991 let overnight = Span::new(1320, 1560);
992 assert_eq!(overnight.length(), 240);
993 assert!(overnight.holds(1500));
994 assert!(!overnight.holds(1200));
995 }
996
997 #[test]
998 fn overlap_is_computed_rather_than_declared() {
999 // The reason Placement carries no `conflicts` flag: the times already
1000 // say it, and a second source for one fact is how a stale conflict
1001 // badge outlives the conflict.
1002 let morning = Placement::new(540, 60); // 09:00-10:00
1003 let overlapping = Placement::new(570, 60); // 09:30-10:30
1004 let after = Placement::new(600, 60); // 10:00-11:00
1005
1006 assert!(morning.overlaps(overlapping));
1007 assert!(overlapping.overlaps(morning), "overlap is symmetric");
1008 // Touching end to end is not overlapping: `to` is exclusive, so a
1009 // 10:00 start does not collide with a 10:00 end.
1010 assert!(!morning.overlaps(after));
1011 assert!(!after.overlaps(morning));
1012 }
1013
1014 #[test]
1015 fn a_placement_is_always_drawable() {
1016 assert_eq!(Placement::new(540, 0).length(), 1);
1017 assert_eq!(Placement::new(540, 30).end(), 570);
1018 }
1019
1020 #[test]
1021 fn a_track_places_the_fraction_every_renderer_would_otherwise_compute() {
1022 let day = Track::DAY;
1023 assert!((day.fraction(0) - 0.0).abs() < f32::EPSILON);
1024 assert!((day.fraction(720) - 0.5).abs() < f32::EPSILON);
1025 // Clamped rather than off the end: an event running past the span's
1026 // close draws at the edge, which beats panicking or drawing nowhere.
1027 assert!((day.fraction(2000) - 1.0).abs() < f32::EPSILON);
1028 }
1029
1030 #[test]
1031 fn a_track_counts_its_slots_and_never_divides_by_zero() {
1032 assert_eq!(Track::DAY.slots(), 96);
1033 assert_eq!(Track::over(Span::new(540, 1020)).slots(), 32);
1034 // A span that does not divide evenly keeps a slot for its tail.
1035 assert_eq!(Track::over(Span::new(0, 50)).slots(), 4);
1036 // slot: 0 is a caller bug that reads as one slot, not a panic.
1037 let degenerate = Track {
1038 span: Span::DAY,
1039 slot: 0,
1040 tick: 60,
1041 unit: Unit::Minutes,
1042 };
1043 assert_eq!(degenerate.slots(), 1);
1044 }
1045
1046 #[test]
1047 fn a_track_carries_facts_and_no_presentation() {
1048 // The guard on the thing the withdrawn refusal was right about. If a
1049 // pixel measure, a scroll offset or a colour ever lands on Track, the
1050 // member has stopped being a fact about the data and the timeline
1051 // really has become a component library wearing a description's name.
1052 let day = Track::DAY;
1053 assert_eq!(day.span, Span::DAY);
1054 assert_eq!(day.slot, 15);
1055 assert_eq!(day.tick, 60);
1056 assert_eq!(day.unit, Unit::Minutes);
1057
1058 // Three fields when this was written, and the fourth came here to say
1059 // why, which is the whole point of the assertion. `unit` is what the
1060 // integers COUNT -- a fact about the data, unavailable from the numbers
1061 // themselves, and the absence of it is what let a month strip render
1062 // under a wall clock. A fifth field still has to argue, and "the
1063 // renderer would find it handy" is still not the argument.
1064 // Destructured rather than rebuilt: this is the form that names every
1065 // field and stops compiling when a fifth arrives, without binding
1066 // anything a lint has to forgive.
1067 let Track {
1068 span: _,
1069 slot: _,
1070 tick: _,
1071 unit: _,
1072 } = day;
1073 }
1074
1075 #[test]
1076 fn a_day_strip_is_the_same_arithmetic_under_a_different_unit() {
1077 // The probe that found the defect, kept as a test. Fifteen days from
1078 // day three, on a thirty-one day month: the geometry was always right
1079 // and only the label was wrong, which is why `unit` is a fact and not
1080 // presentation.
1081 let march = Track::days(Span::new(0, 31));
1082 assert_eq!(march.slots(), 31);
1083 assert_eq!(march.unit, Unit::Days);
1084
1085 let leave = Placement::new(2, 15);
1086 assert!((march.fraction(leave.at()) - 2.0 / 31.0).abs() < 0.0001);
1087 assert!((march.fraction(leave.end()) - 17.0 / 31.0).abs() < 0.0001);
1088 }
1089
1090 #[test]
1091 fn a_pane_is_looked_into_and_a_band_is_not() {
1092 assert_eq!(Region::Pane.depth(), Depth::Well);
1093 assert_eq!(Region::Modal.depth(), Depth::Raised);
1094 for r in [
1095 Region::Band,
1096 Region::Sidebar,
1097 Region::Group,
1098 Region::Split,
1099 Region::TabGroup,
1100 ] {
1101 assert_eq!(r.depth(), Depth::Flat, "{r:?} should carry no edge");
1102 }
1103 }
1104
1105 #[test]
1106 fn exactly_one_region_is_opaque() {
1107 // The escape hatch is one member and stays one member. If a second
1108 // undescribed region ever appears, the description has started
1109 // conceding rather than deferring.
1110 for r in [
1111 Region::Band,
1112 Region::Sidebar,
1113 Region::Pane,
1114 Region::Group,
1115 Region::Split,
1116 Region::TabGroup,
1117 Region::Modal,
1118 // A widget is described, and that is the whole of what separates it
1119 // from a bespoke here. Both carry a name this crate never reads;
1120 // only one of them has contents under it that a renderer which does
1121 // not know the name can still walk.
1122 Region::Widget { name: "carousel" },
1123 ] {
1124 assert!(r.described(), "{r:?} should be describable");
1125 }
1126 assert!(!Region::Handover { name: "day-plan" }.described());
1127 }
1128
1129 #[test]
1130 fn a_region_nobody_converted_yet_is_not_a_region_the_app_gave_up_on() {
1131 // The whole of why this is two members. Both are opaque and neither is
1132 // described, so the old single member made them one value; a renderer
1133 // with no fill drew both as an empty box and said nothing either way.
1134 assert!(Region::Handover { name: "day-plan" }.owed());
1135 assert!(
1136 !Region::Ceded {
1137 name: "revenue-chart"
1138 }
1139 .owed()
1140 );
1141
1142 // Everything that carries its own contents owes nothing, which is the
1143 // reading a renderer needs for the members it already draws.
1144 assert!(!Region::Pane.owed());
1145 assert!(!Region::Widget { name: "carousel" }.owed());
1146
1147 // Opaqueness is the axis they still share.
1148 assert!(!Region::Ceded { name: "waveform" }.described());
1149 assert_eq!(Region::Ceded { name: "waveform" }.name(), Some("waveform"));
1150 assert_eq!(Region::Ceded { name: "waveform" }.depth(), Depth::Flat);
1151 }
1152
1153 #[test]
1154 fn a_group_contains_a_section_without_claiming_to_be_a_pane() {
1155 // The whole of why this is a member rather than a `Pane`. A pane is
1156 // looked into and scrolls; a group is neither, and four groups inside a
1157 // settings pane described as panes are four wells inside a well.
1158 assert_eq!(Region::Pane.depth(), Depth::Well);
1159 assert_eq!(Region::Group.depth(), Depth::Flat);
1160 assert_ne!(Region::Group, Region::Pane);
1161
1162 // Described, and it carries no name: a group is a primitive every
1163 // renderer draws from scratch, which is what separates it from the two
1164 // members that do carry one.
1165 assert!(Region::Group.described());
1166 assert_eq!(Region::Group.name(), None);
1167 }
1168
1169 #[test]
1170 fn a_section_heading_names_a_block_that_now_exists() {
1171 // `Heading::Section` has said "names a block within the screen" since
1172 // 0.2.0 and there was no block. The pairing is the point, and it is the
1173 // reason a group carries no heading of its own: the heading is an
1174 // ordinary node in the body, and a group without one is legal.
1175 assert!(Heading::Section.separated());
1176 assert_eq!(Region::Group.depth(), Depth::Flat);
1177 }
1178
1179 #[test]
1180 fn a_widget_inherits_its_depth_the_way_a_bespoke_does() {
1181 // Stronger than the bespoke case: a widget is drawn by whichever
1182 // renderer recognises the name, so a depth chosen here would be this
1183 // crate deciding a carousel is raised on every host.
1184 assert_eq!(Region::Widget { name: "carousel" }.depth(), Depth::Flat);
1185 assert_eq!(Region::Widget { name: "pager" }.depth(), Depth::Flat);
1186 }
1187
1188 #[test]
1189 fn a_name_is_readable_without_asking_which_member_carried_it() {
1190 // A renderer dispatching on a name wants the string, not the member.
1191 // Writing that `matches!` at each renderer is how the two drift apart.
1192 assert_eq!(Region::Widget { name: "carousel" }.name(), Some("carousel"));
1193 assert_eq!(
1194 Region::Handover { name: "day-plan" }.name(),
1195 Some("day-plan")
1196 );
1197
1198 for r in [
1199 Region::Band,
1200 Region::Sidebar,
1201 Region::Pane,
1202 Region::Group,
1203 Region::Split,
1204 Region::TabGroup,
1205 Region::Modal,
1206 ] {
1207 assert_eq!(r.name(), None, "{r:?} names nothing an app chose");
1208 }
1209 }
1210
1211 #[test]
1212 fn a_bespoke_region_inherits_its_depth_rather_than_choosing_one() {
1213 // The app owns the contents, not the placement. An app that wants its
1214 // timeline in a well frames it in a Pane.
1215 assert_eq!(Region::Handover { name: "day-plan" }.depth(), Depth::Flat);
1216 assert_eq!(Region::Handover { name: "kanban" }.depth(), Depth::Flat);
1217 }
1218
1219 #[test]
1220 fn a_screen_with_a_bespoke_region_is_still_a_whole_screen() {
1221 // The argument the member exists for: goingson's day-plan has to be
1222 // routable, or the description covers only the boring screens and the
1223 // interesting four need a second path beside the router.
1224 let day_plan = [
1225 Region::Band,
1226 Region::Handover { name: "day-plan" },
1227 Region::Sidebar,
1228 ];
1229 assert_eq!(day_plan.iter().filter(|r| r.described()).count(), 2);
1230 assert_eq!(day_plan.iter().filter(|r| !r.described()).count(), 1);
1231 }
1232
1233 #[test]
1234 fn a_secret_field_is_marked_as_one_and_a_hidden_field_is_not_drawn() {
1235 let secret = Field::new(FieldKind::Secret, "password", "Password");
1236 assert!(secret.kind.confidential());
1237 assert!(secret.kind.visible());
1238
1239 assert!(!FieldKind::Hidden.visible());
1240 // Nothing else is confidential, or the marker means nothing.
1241 for k in [
1242 FieldKind::Text,
1243 FieldKind::Number,
1244 FieldKind::Textarea,
1245 FieldKind::Rich,
1246 FieldKind::Select,
1247 FieldKind::Checkbox,
1248 FieldKind::Hidden,
1249 ] {
1250 assert!(!k.confidential(), "{k:?} should not be confidential");
1251 }
1252
1253 // Only a checkbox carries its own label.
1254 assert!(FieldKind::Checkbox.labels_itself());
1255 assert!(!FieldKind::Text.labels_itself());
1256 }
1257
1258 #[test]
1259 fn a_plain_field_offers_nothing_and_a_select_offers_its_options() {
1260 let text = Field::new(FieldKind::Text, "title", "Title");
1261 assert!(text.options.is_empty());
1262 assert_eq!(text.placeholder, None);
1263
1264 let sizes = [Choice::plain("small"), Choice::plain("large")];
1265 let select = Field::select("size", "Size", &sizes);
1266 assert_eq!(select.kind, FieldKind::Select);
1267 assert_eq!(select.options.len(), 2);
1268 }
1269
1270 #[test]
1271 fn a_choice_says_what_submits_and_what_is_read_apart() {
1272 // The whole reason it is two strings. `plain` is the case where they
1273 // coincide, and it is a shorthand rather than the general shape.
1274 let plain = Choice::plain("7");
1275 assert_eq!((plain.value, plain.label), ("7", "7"));
1276
1277 let spelled = Choice::new("7", "One week");
1278 assert_ne!(spelled.value, spelled.label);
1279 assert!(
1280 spelled.available(),
1281 "an option is pickable until it says not"
1282 );
1283 }
1284
1285 #[test]
1286 fn a_candidate_carries_the_line_that_tells_it_from_its_neighbours() {
1287 // The gap this type was born for: two candidates whose labels read
1288 // alike, told apart by the second string and by nothing else. The
1289 // measured site is the MNW tag box, where "Format" is a leaf under
1290 // audio, software, writing and video.
1291 let audio = Candidate::new("audio/format", "Format").detailed("Audio");
1292 let writing = Candidate::new("writing/format", "Format").detailed("Writing");
1293
1294 assert_eq!(audio.label, writing.label);
1295 assert_ne!(audio.detail, writing.detail);
1296 assert_ne!(
1297 audio, writing,
1298 "two rows a user cannot tell apart are two rows the type can"
1299 );
1300 }
1301
1302 #[test]
1303 fn a_candidate_is_read_differently_from_an_option_and_written_the_same() {
1304 // The ruling's own distinction, held as a test so the two types do not
1305 // drift back together. Submitting is identical; the second line is the
1306 // whole of what differs, and it is absent by default because a list of
1307 // distinct labels wants nothing there.
1308 let candidate = Candidate::plain("rust");
1309 assert_eq!((candidate.value, candidate.label), ("rust", "rust"));
1310 assert_eq!(
1311 candidate.detail, None,
1312 "one line unless the route says otherwise"
1313 );
1314
1315 let option = Choice::plain("rust");
1316 assert_eq!(
1317 (candidate.value, candidate.label),
1318 (option.value, option.label)
1319 );
1320 }
1321
1322 #[test]
1323 fn a_radio_asks_the_same_question_as_a_select_and_is_not_the_same_kind() {
1324 // Both offer a fixed set and both read `options`, so the two
1325 // constructors differ in exactly one thing. That one thing is the
1326 // point: a renderer decides whether the alternatives are readable
1327 // without opening anything, and it can only decide that if the
1328 // description said which question was asked.
1329 let styles = [
1330 Choice::new("copy", "Copy samples in"),
1331 Choice::new("reference", "Reference in place"),
1332 ];
1333 let radio = Field::radio("storage", "Storage style", &styles);
1334 let select = Field::select("storage", "Storage style", &styles);
1335
1336 assert_eq!(radio.kind, FieldKind::Radio);
1337 assert_ne!(radio.kind, select.kind);
1338 assert_eq!(radio.options, select.options);
1339 assert_eq!(
1340 Field {
1341 kind: select.kind,
1342 ..radio
1343 },
1344 select
1345 );
1346 }
1347
1348 #[test]
1349 fn an_unavailable_option_cannot_be_silent_about_it() {
1350 // The whole content of the one-member shape: saying an option is not
1351 // pickable and saying why are the same act, so the greyed-out-with-no-
1352 // reason state is unsayable rather than merely discouraged.
1353 let multi =
1354 Choice::new("multi", "Multi-sample").unless("Drop a second sample onto the keyboard.");
1355 assert!(!multi.available());
1356 assert_eq!(
1357 multi.unavailable,
1358 Some("Drop a second sample onto the keyboard.")
1359 );
1360
1361 // And the option is still in the list, carrying what it submits, so a
1362 // renderer draws it rather than the app dropping it.
1363 assert_eq!(multi.value, "multi");
1364 assert_eq!(multi.label, "Multi-sample");
1365 }
1366
1367 #[test]
1368 fn an_option_can_say_what_picking_it_means_and_why_it_cannot_be_picked() {
1369 // `5e21dcfc`. Two different sentences about one option, and an option
1370 // that has both has said two things: what the tier is, and that it is
1371 // not available yet. Folding them would be the label-folding this
1372 // member exists to end.
1373 let tier = Choice::new("24", "Small Files")
1374 .detailing("$24/mo. 2GB/file, 100GB total. Fits audio, plugins, binaries.")
1375 .unless("Sold out while the founder window is open.");
1376
1377 assert_eq!(
1378 tier.detail,
1379 Some("$24/mo. 2GB/file, 100GB total. Fits audio, plugins, binaries.")
1380 );
1381 assert_eq!(
1382 tier.unavailable,
1383 Some("Sold out while the founder window is open.")
1384 );
1385 assert!(!tier.available());
1386
1387 // Neither is implied by the other, which is what keeps a renderer from
1388 // reading a detail as a reason: an ordinary option with a second line
1389 // is still pickable.
1390 let plain = Choice::new("free", "Free").detailing("No charge. Available to everyone.");
1391 assert!(plain.available());
1392 assert_eq!(plain.detail, Some("No charge. Available to everyone."));
1393 assert_eq!(Choice::new("free", "Free").detail, None);
1394 }
1395
1396 #[test]
1397 fn a_range_carries_both_ends_and_a_validated_number_need_not() {
1398 // The distinction the kind exists for, asserted rather than only
1399 // written down: bounds are a rule for one and the control itself for
1400 // the other.
1401 let threshold = Field::range("review", "Review above", "0", "1");
1402 assert_eq!(threshold.kind, FieldKind::Range);
1403 assert!(threshold.bounded());
1404 assert_eq!(threshold.min, Some("0"));
1405 assert_eq!(threshold.max, Some("1"));
1406 // Granularity is the host's until an app says otherwise.
1407 assert_eq!(threshold.step, None);
1408
1409 // goingson's duration: a typed number with a floor, and it must not
1410 // read as a slider.
1411 let minutes = Field {
1412 min: Some("1"),
1413 ..Field::new(FieldKind::Number, "minutes", "Minutes")
1414 };
1415 assert_ne!(minutes.kind, FieldKind::Range);
1416 assert!(!minutes.bounded(), "one end is a rule, not an extent");
1417 }
1418
1419 #[test]
1420 fn a_range_described_with_one_end_says_so_rather_than_being_refused() {
1421 // Nothing here enforces the pair, for the reason nothing here enforces
1422 // `required`: the description states the constraint and the renderer
1423 // asks. What it must not do is look bounded.
1424 let half = Field {
1425 max: Some("1"),
1426 ..Field::new(FieldKind::Range, "review", "Review above")
1427 };
1428 assert!(!half.bounded());
1429 }
1430
1431 #[test]
1432 fn exactly_the_option_taking_kinds_say_so() {
1433 // The renderers branch on this rather than on a list of their own, so
1434 // a kind added without a decision here renders its options nowhere.
1435 assert!(FieldKind::Select.offers_options());
1436 assert!(FieldKind::Radio.offers_options());
1437 for kind in [
1438 FieldKind::Text,
1439 FieldKind::Secret,
1440 FieldKind::Number,
1441 FieldKind::Email,
1442 FieldKind::Url,
1443 FieldKind::Tel,
1444 FieldKind::Range,
1445 FieldKind::Textarea,
1446 FieldKind::Rich,
1447 FieldKind::Checkbox,
1448 FieldKind::Hidden,
1449 ] {
1450 assert!(!kind.offers_options(), "{kind:?} does not offer options");
1451 }
1452 }
1453
1454 #[test]
1455 fn a_radio_group_takes_a_label_even_though_its_options_carry_their_own() {
1456 // The near-miss: each option is labelled beside its own button, so a
1457 // renderer could plausibly read the group as self-labelling and drop
1458 // the question. Checkbox is the only kind that does that.
1459 assert!(!FieldKind::Radio.labels_itself());
1460 assert!(FieldKind::Checkbox.labels_itself());
1461 }
1462
1463 #[test]
1464 fn a_select_with_no_options_is_sayable() {
1465 // An app whose option list has not loaded has exactly this. Making it
1466 // unrepresentable would push the state somewhere less visible, and a
1467 // renderer drawing an empty select reports it on screen.
1468 let loading = Field::select("project", "Project", &[]);
1469 assert!(loading.options.is_empty());
1470 }
1471
1472 #[test]
1473 fn the_description_carries_the_question_and_never_the_answer() {
1474 // The line 0.8.0 drew. Placeholder and options are properties of what
1475 // is being asked; the current value is what came back, and no field
1476 // here holds one.
1477 let f = Field {
1478 placeholder: Some("yyyy-mm-dd"),
1479 ..Field::new(FieldKind::Text, "due", "Due")
1480 };
1481 assert_eq!(f.placeholder, Some("yyyy-mm-dd"));
1482 // A placeholder is not a label, and having one does not excuse the
1483 // field from carrying the other.
1484 assert_eq!(f.label, "Due");
1485 }
1486
1487 #[test]
1488 fn a_field_reports_its_own_error_state() {
1489 let mut f = Field::new(FieldKind::Text, "title", "Title");
1490 assert!(!f.invalid());
1491 f.error = Some("Required");
1492 assert!(f.invalid());
1493 }
1494
1495 #[test]
1496 fn columns_drop_by_priority_and_never_by_position() {
1497 let cols = [
1498 Column {
1499 width: Width::Fill,
1500 priority: Priority::Essential,
1501 ..Column::new("Title")
1502 },
1503 Column {
1504 width: Width::Fixed,
1505 priority: Priority::Secondary,
1506 ..Column::new("Due")
1507 },
1508 Column {
1509 width: Width::Fixed,
1510 priority: Priority::Optional,
1511 ..Column::new("Estimate")
1512 },
1513 ];
1514
1515 // Widest: everything survives.
1516 assert_eq!(
1517 cols.iter()
1518 .filter(|c| c.kept_at(Priority::Optional))
1519 .count(),
1520 3
1521 );
1522 // Narrower: the optional column goes first.
1523 let kept: Vec<_> = cols
1524 .iter()
1525 .filter(|c| c.kept_at(Priority::Secondary))
1526 .map(|c| c.name)
1527 .collect();
1528 assert_eq!(kept, ["Title", "Due"]);
1529 // Narrowest: only what identifies the row.
1530 let kept: Vec<_> = cols
1531 .iter()
1532 .filter(|c| c.kept_at(Priority::Essential))
1533 .map(|c| c.name)
1534 .collect();
1535 assert_eq!(kept, ["Title"]);
1536 }
1537
1538 #[test]
1539 fn inserting_a_column_does_not_move_what_gets_dropped() {
1540 // The bug the ordinal form has and this form cannot: goingson hides
1541 // `nth-child(n+5)` against a seven-column table, so a column inserted
1542 // anywhere to the left silently hides a different one.
1543 let before = [
1544 Column::new("Title"),
1545 Column {
1546 width: Width::Fixed,
1547 priority: Priority::Optional,
1548 ..Column::new("Estimate")
1549 },
1550 ];
1551 let after = [
1552 Column::new("Title"),
1553 Column::new("Project"), // inserted
1554 Column {
1555 width: Width::Fixed,
1556 priority: Priority::Optional,
1557 ..Column::new("Estimate")
1558 },
1559 ];
1560
1561 fn dropped<'a>(cols: &[Column<'a>]) -> Vec<&'a str> {
1562 cols.iter()
1563 .filter(|c| !c.kept_at(Priority::Secondary))
1564 .map(|c| c.name)
1565 .collect()
1566 }
1567 assert_eq!(dropped(&before), ["Estimate"]);
1568 assert_eq!(dropped(&after), ["Estimate"]);
1569 }
1570
1571 #[test]
1572 fn an_arrangement_carries_the_tab_group_as_a_modifier() {
1573 // goingson uses the tab group inside the content region rather than
1574 // instead of one, so it is not a third arrangement.
1575 let go = Arrangement::list_detail(true);
1576 let plain = Arrangement::list_detail(false);
1577 assert_ne!(go, plain);
1578 assert_ne!(go, Arrangement::sidebar_content());
1579 }
1580
1581 #[test]
1582 fn a_share_is_a_proportion_and_resolves_the_same_way_everywhere() {
1583 // The point of the member: a terminal reading columns and a webview
1584 // reading a grid honour one fact, so two hosts showing one screen agree
1585 // about its proportions.
1586 assert_eq!(Share::LIST.as_percent(), 40);
1587 assert_eq!(Share::LIST.of(100), 40);
1588 assert_eq!(
1589 Share::SIDEBAR.of(96),
1590 24,
1591 "quasi-tui's 24 columns, said as a quarter"
1592 );
1593 }
1594
1595 #[test]
1596 fn a_region_never_resolves_to_nothing() {
1597 // A region the description named should be visible. A zero-width one
1598 // reads on screen as a region that vanished, which is the hardest kind
1599 // of bug to find from what is drawn.
1600 assert_eq!(Share::percent(5).of(1), 1);
1601 assert_eq!(Share::percent(5).of(0), 1);
1602 }
1603
1604 #[test]
1605 fn a_share_outside_the_range_is_clamped_rather_than_refused() {
1606 assert_eq!(Share::percent(0), Share::percent(5));
1607 assert_eq!(Share::percent(200), Share::percent(95));
1608 }
1609
1610 #[test]
1611 fn the_share_rides_on_the_arrangement_that_knows_which_question_it_is() {
1612 // How much a sidebar takes and how much a list side takes are different
1613 // questions, and this enum is the only thing that knows which is being
1614 // asked.
1615 assert_eq!(Arrangement::sidebar_content().share(), Some(Share::SIDEBAR));
1616 assert_eq!(Arrangement::list_detail(false).share(), Some(Share::LIST));
1617 // One region divides nothing, so there is no share to answer with.
1618 assert_eq!(Arrangement::Single.share(), None);
1619 assert_eq!(
1620 Arrangement::Single.with_share(Share::percent(20)),
1621 Arrangement::Single
1622 );
1623
1624 let narrow = Arrangement::sidebar_content().with_share(Share::percent(20));
1625 assert_eq!(narrow.share(), Some(Share::percent(20)));
1626 assert!(matches!(narrow, Arrangement::SidebarContent { .. }));
1627 }
1628
1629 #[test]
1630 fn a_measure_defaults_to_the_one_53_of_69_templates_asked_for() {
1631 // The default is meaningful: a screen nobody said anything about uses
1632 // the window it was given.
1633 assert_eq!(Measure::default(), Measure::Wide);
1634 assert_eq!(Measure::Reading.as_str(), "reading");
1635 }
1636
1637 #[test]
1638 fn readiness_names_the_state_and_not_the_shimmer() {
1639 // Two members and no third. If a skeleton ever appears in this enum,
1640 // the deferral rule has been broken.
1641 assert_ne!(Readiness::Ready, Readiness::Pending);
1642 }
1643
1644 #[test]
1645 fn a_window_with_no_length_still_answers_what_it_can() {
1646 // The uncounted case is the common one, not the degenerate one: a query
1647 // that asked for 51 to learn there were more than 50 knows there are,
1648 // and not how many.
1649 let uncounted = Window::new(100, 50);
1650 assert_eq!(uncounted.index(), Some(2));
1651 assert_eq!(uncounted.windows(), None);
1652 assert!(uncounted.has_before());
1653 // Unknown length cannot rule out more, and offering a way forward that
1654 // turns out empty is the cheaper mistake.
1655 assert!(uncounted.has_after());
1656 }
1657
1658 #[test]
1659 fn a_counted_window_knows_where_it_ends() {
1660 let last = Window::new(350, 50).of(400);
1661 assert_eq!(last.index(), Some(7));
1662 assert_eq!(last.windows(), Some(8));
1663 assert!(last.has_before());
1664 assert!(!last.has_after());
1665
1666 let first = Window::new(0, 50).of(400);
1667 assert!(!first.has_before());
1668 assert!(first.has_after());
1669 }
1670
1671 #[test]
1672 fn a_window_that_does_not_divide_evenly_rounds_up() {
1673 // 401 rows in pages of 50 is eight pages and a straggler, which is nine
1674 // pages. Rounding down would make the last one unreachable.
1675 assert_eq!(Window::new(0, 50).of(401).windows(), Some(9));
1676 }
1677
1678 #[test]
1679 fn a_zero_count_answers_none_rather_than_dividing() {
1680 let empty = Window::new(0, 0).of(400);
1681 assert_eq!(empty.index(), None);
1682 assert_eq!(empty.windows(), None);
1683 // And it still clamps rather than panicking.
1684 assert_eq!(Window::new(900, 0).of(400).clamped().from, 399);
1685 }
1686
1687 #[test]
1688 fn a_window_past_the_end_clamps_inside_rather_than_vanishing() {
1689 // `Slot::current`'s reasoning, one layer down: a description pointing
1690 // past the end is a host bug, and answering it by drawing nothing
1691 // reports a region that vanished.
1692 assert_eq!(Window::new(900, 50).of(400).clamped().from, 350);
1693 // Nothing to clamp against when the length is unknown.
1694 assert_eq!(Window::new(900, 50).clamped().from, 900);
1695 }
1696
1697 #[test]
1698 fn a_carousel_frame_is_a_window_of_one() {
1699 // The shape a carousel instantiates. Same code as a paged list, which is
1700 // the whole reason `Window` exists rather than two copies of it.
1701 let third = Window::frame(2, 5);
1702 assert_eq!(third.index(), Some(2));
1703 assert_eq!(third.windows(), Some(5));
1704 assert!(third.has_before());
1705 assert!(third.has_after());
1706
1707 let last = Window::frame(4, 5);
1708 assert!(!last.has_after());
1709 }
1710
1711 #[test]
1712 fn numbered_pages_read_from_one_and_load_more_has_no_page() {
1713 // The page number is read aloud, so it is one-based; `Window::index` is
1714 // the zero-based form for indexing.
1715 let third = Paging::pages(100, 50).of(400);
1716 assert_eq!(third.page(), Some(3));
1717 assert_eq!(third.pages_total(), Some(8));
1718 assert_eq!(third.total(), Some(400));
1719 assert!(third.has_previous());
1720 assert!(third.has_more());
1721
1722 // Load-more grew a window from the start, so "page 2" would name
1723 // nothing and the type says so rather than inventing one.
1724 let grown = Paging::more(150).of(400);
1725 assert_eq!(grown.page(), None);
1726 assert_eq!(grown.pages_total(), None);
1727 assert_eq!(grown.shown(), 150);
1728 assert!(!grown.has_previous());
1729 assert!(grown.has_more());
1730 }
1731
1732 #[test]
1733 fn an_uncounted_paging_offers_forward_and_admits_no_total() {
1734 // What a host that will not pay for a COUNT describes. `None` here is
1735 // permanent: a total arriving later would widen the text that prints it,
1736 // which is the reflow "first paint is final paint" forbids.
1737 let feed = Paging::more(50);
1738 assert_eq!(feed.total(), None);
1739 assert_eq!(feed.pages_total(), None);
1740 assert_eq!(feed.remaining(), None);
1741 assert!(feed.has_more());
1742 }
1743
1744 #[test]
1745 fn what_is_left_is_derived_and_never_underflows() {
1746 assert_eq!(Paging::more(150).of(400).remaining(), Some(250));
1747 assert_eq!(Paging::pages(350, 50).of(400).remaining(), Some(0));
1748 // A host that overshot its own total gets zero rather than a wrapped
1749 // usize, which would print as "18446744073709551516 remaining".
1750 assert_eq!(Paging::more(500).of(400).remaining(), Some(0));
1751 }
1752
1753 #[test]
1754 fn a_fallback_is_authored_and_a_group_cannot_omit_it() {
1755 // No `Default`. The compiler is what enforces rule 2, so the assertion
1756 // that matters is one this file cannot write; what it can say is that
1757 // the four authored answers are distinct and none is privileged.
1758 let all = [
1759 Fallback::Wrap,
1760 Fallback::Stack,
1761 Fallback::Shed,
1762 Fallback::Menu,
1763 ];
1764 for (i, a) in all.iter().enumerate() {
1765 for b in &all[i + 1..] {
1766 assert_ne!(a, b);
1767 }
1768 }
1769 }
1770
1771 #[test]
1772 fn shedding_stops_at_essential_whatever_the_group_holds() {
1773 // Priority is read the same way for a group member as for a column,
1774 // which is the whole claim of generalising it off `Column`.
1775 let members = [
1776 ("tabs", Priority::Essential),
1777 ("search", Priority::Secondary),
1778 ("count", Priority::Optional),
1779 ];
1780 let kept: Vec<_> = members
1781 .iter()
1782 .filter(|(_, p)| *p >= Priority::Essential)
1783 .map(|(n, _)| *n)
1784 .collect();
1785 assert_eq!(kept, ["tabs"]);
1786 }
1787
1788 #[test]
1789 fn a_role_says_what_a_part_is_worth_when_the_run_does_not_fit() {
1790 // The row still identifies itself after everything droppable has gone,
1791 // which is the property the ladder exists for.
1792 assert_eq!(RowPart::Primary.priority(), Priority::Essential);
1793 // A control is not a fact. Room comes out of what the row says, never
1794 // out of what it offers.
1795 assert_eq!(RowPart::Actions.priority(), Priority::Essential);
1796 assert_eq!(RowPart::Meta.priority(), Priority::Optional);
1797 assert_eq!(RowPart::Proportion.priority(), Priority::Optional);
1798 assert_eq!(RowPart::Secondary.priority(), Priority::Secondary);
1799 // Tokens sit in the middle deliberately: a toned badge is often the
1800 // most scannable thing in a row, so it does not go first.
1801 assert_eq!(RowPart::Tokens.priority(), Priority::Secondary);
1802 }
1803
1804 #[test]
1805 fn a_run_is_one_line_unless_the_description_says_two() {
1806 // The default is what every part did before flows existed, so a
1807 // description written against the old vocabulary keeps its rendering.
1808 assert_eq!(Flow::default(), Flow::Tight);
1809 assert_eq!(Flow::Tight.lines(), 1);
1810 assert_eq!(Flow::Relaxed.lines(), 2);
1811 }
1812
1813 #[test]
1814 fn an_unknown_flow_reads_as_one_line() {
1815 // `#[non_exhaustive]`'s cost, taken deliberately. A tier added upstream
1816 // reaches an old renderer as one line rather than as a build break, and
1817 // one line is the reading that cannot break a neighbour's layout. The
1818 // match in `lines` is what this holds; it fails if a new tier is given
1819 // an arm that returns something unbounded.
1820 for flow in [Flow::Tight, Flow::Relaxed] {
1821 assert!((1..=2).contains(&flow.lines()));
1822 }
1823 }
1824
1825 #[test]
1826 fn an_awaiting_mark_is_indeterminate_until_something_is_measured() {
1827 // The default is the common case: a call waits, and nothing about it is
1828 // countable. A determinate bar is the exception and says so.
1829 assert_eq!(Awaiting::default(), Awaiting::unmeasured());
1830 assert!(!Awaiting::unmeasured().is_determinate());
1831 assert!(Awaiting::of(40 * 1024 * 1024).is_determinate());
1832 assert_eq!(Awaiting::of(7).amount, Some(7));
1833 }
1834
1835 // A slider is a fraction and a mapping
1836 //
1837 // `Curve` is the one thing in this crate that computes rather than
1838 // describes, and it does so because four renderers would otherwise each
1839 // write these two formulas and drift. So the formulas are pinned here.
1840
1841 /// The bounds of audiofiles' envelope attack, the curve's first consumer.
1842 const ATTACK: (f64, f64) = (0.001, 5.0);
1843
1844 #[test]
1845 fn a_curve_is_linear_with_no_step_until_a_field_says_otherwise() {
1846 assert_eq!(Curve::default(), Curve::Linear { step: None });
1847 let plain = Field::range("t", "T", "0", "1");
1848 assert_eq!(plain.curve, Curve::Linear { step: None });
1849 assert_eq!(plain.curve.step(), None);
1850 }
1851
1852 #[test]
1853 fn every_curve_carries_its_own_granularity() {
1854 assert_eq!(Curve::Linear { step: Some("0.01") }.step(), Some("0.01"));
1855 assert_eq!(
1856 Curve::Logarithmic {
1857 step: Some("0.001")
1858 }
1859 .step(),
1860 Some("0.001")
1861 );
1862 }
1863
1864 #[test]
1865 fn both_ends_of_the_track_are_the_bounds_under_either_curve() {
1866 // `min` and `max` are `f(0)` and `f(1)`. That is the whole reframe, and
1867 // it has to hold for a mapping that is not the identity or the bounds
1868 // have stopped meaning what the field says they mean.
1869 let (min, max) = ATTACK;
1870 for curve in [
1871 Curve::Linear { step: None },
1872 Curve::Logarithmic { step: None },
1873 ] {
1874 assert!((curve.value_at(0.0, min, max) - min).abs() < 1e-12);
1875 assert!((curve.value_at(1.0, min, max) - max).abs() < 1e-12);
1876 }
1877 }
1878
1879 #[test]
1880 fn a_linear_midpoint_is_the_average_and_a_ratio_midpoint_is_the_geometric_mean() {
1881 let (min, max) = ATTACK;
1882 let linear = Curve::Linear { step: None }.value_at(0.5, min, max);
1883 assert!((linear - 2.5005).abs() < 1e-9);
1884
1885 // The reason the envelope is not linear: half way along a log track is
1886 // 70 ms, and half way along a linear one is 2.5 seconds. Every attack a
1887 // sampler is actually played with lives below the first.
1888 let ratio = Curve::Logarithmic { step: None }.value_at(0.5, min, max);
1889 assert!((ratio - (min * max).sqrt()).abs() < 1e-12);
1890 assert!(ratio < 0.08);
1891 }
1892
1893 #[test]
1894 fn a_position_and_a_value_round_trip_under_either_curve() {
1895 let (min, max) = ATTACK;
1896 for curve in [
1897 Curve::Linear { step: None },
1898 Curve::Logarithmic { step: None },
1899 ] {
1900 for position in [0.0, 0.1, 0.25, 0.5, 0.75, 0.99, 1.0] {
1901 let back = curve.position_of(curve.value_at(position, min, max), min, max);
1902 assert!(
1903 (back - position).abs() < 1e-9,
1904 "{curve:?} lost {position} (got {back})"
1905 );
1906 }
1907 }
1908 }
1909
1910 #[test]
1911 fn a_ratio_curve_across_zero_is_drawn_linearly_rather_than_refused() {
1912 // An envelope's sustain is a 0-to-1 level. A constant ratio is
1913 // undefined there, and the answer is the linear mapping rather than a
1914 // NaN reaching a renderer that would paint it.
1915 let curve = Curve::Logarithmic { step: None };
1916 assert!(!curve.is_ratio(0.0, 1.0));
1917 assert!((curve.value_at(0.5, 0.0, 1.0) - 0.5).abs() < 1e-12);
1918 assert!(curve.value_at(0.5, -96.0, -20.0).is_finite());
1919 assert!(curve.is_ratio(ATTACK.0, ATTACK.1));
1920 }
1921
1922 #[test]
1923 fn a_track_with_no_extent_has_one_value_on_it() {
1924 for curve in [
1925 Curve::Linear { step: None },
1926 Curve::Logarithmic { step: None },
1927 ] {
1928 assert!((curve.value_at(0.7, 4.0, 4.0) - 4.0).abs() < f64::EPSILON);
1929 assert!(curve.position_of(4.0, 4.0, 4.0).abs() < f64::EPSILON);
1930 // Inverted bounds are the same degenerate answer, not a negative
1931 // extent a renderer would draw backwards.
1932 assert!((curve.value_at(0.7, 9.0, 2.0) - 9.0).abs() < f64::EPSILON);
1933 }
1934 }
1935
1936 #[test]
1937 fn a_position_or_a_value_outside_the_track_is_clamped_to_it() {
1938 let (min, max) = ATTACK;
1939 let curve = Curve::Logarithmic { step: None };
1940 assert!((curve.value_at(-3.0, min, max) - min).abs() < 1e-12);
1941 assert!((curve.value_at(4.0, min, max) - max).abs() < 1e-12);
1942 assert!(curve.position_of(0.0, min, max).abs() < 1e-12);
1943 assert!((curve.position_of(500.0, min, max) - 1.0).abs() < 1e-12);
1944 }
1945
1946 #[test]
1947 fn a_typed_number_keeps_its_own_step_and_a_range_reads_its_curve() {
1948 // The split the 0.32.0 narrowing is: two granularities that were one
1949 // member, and the kinds that take them do not overlap.
1950 let typed = Field {
1951 step: Some("5"),
1952 ..Field::new(FieldKind::Number, "port", "Port")
1953 };
1954 assert_eq!(typed.step, Some("5"));
1955
1956 let slid = Field {
1957 curve: Curve::Logarithmic {
1958 step: Some("0.001"),
1959 },
1960 ..Field::range("attack", "Attack", "0.001", "5")
1961 };
1962 assert_eq!(slid.step, None);
1963 assert_eq!(slid.curve.step(), Some("0.001"));
1964 }
1965
1966 #[test]
1967 fn a_theme_picker_offers_themes_and_no_options() {
1968 // The substitution hazard the constructor exists against: `options` is
1969 // right there and reads as if it would work, and a renderer walking it
1970 // for a theme picker draws an empty control.
1971 let themes = [
1972 ThemeChoice::new("goingson", "GoingsOn", ThemeVariant::Light, Contrast::High),
1973 ThemeChoice::new("dracula", "Dracula", ThemeVariant::Dark, Contrast::Standard),
1974 ];
1975 let field = Field::theme("theme", "Theme", &themes);
1976
1977 assert_eq!(field.kind, FieldKind::Theme);
1978 assert!(field.kind.offers_themes());
1979 assert!(!field.kind.offers_options());
1980 assert_eq!(field.themes.len(), 2);
1981 assert!(field.options.is_empty());
1982 assert_eq!(field.follows, None);
1983 }
1984
1985 #[test]
1986 fn following_carries_the_store_s_own_spelling() {
1987 // Not hardcoded here: the value belongs to the app's config table, and
1988 // this crate holds no facts about somebody else's store.
1989 let field =
1990 Field::theme("theme", "Theme", &[]).following(Choice::new("system", "Follow System"));
1991 let follow = field.follows.expect("the row was offered");
1992 assert_eq!(follow.value, "system");
1993 assert_eq!(follow.label, "Follow System");
1994 }
1995
1996 #[test]
1997 fn a_picker_with_nothing_resolved_is_sayable() {
1998 // A machine whose theme directories hold nothing. The description is
1999 // true and a renderer says so on screen rather than in a log, which is
2000 // `Field::options`' own arrangement.
2001 let field = Field::theme("theme", "Theme", &[]);
2002 assert!(field.themes.is_empty());
2003 }
2004
2005 #[test]
2006 fn every_kind_but_theme_offers_no_themes() {
2007 for kind in [
2008 FieldKind::Text,
2009 FieldKind::Select,
2010 FieldKind::Radio,
2011 FieldKind::Checkbox,
2012 FieldKind::File,
2013 FieldKind::Hidden,
2014 ] {
2015 assert!(
2016 !kind.offers_themes(),
2017 "{kind:?} does not read Field::themes"
2018 );
2019 }
2020 }
2021
2022 #[test]
2023 fn a_contrast_tier_reads_worst_first() {
2024 // Matches `makeover::ContrastTier`, so an adopter's conversion cannot
2025 // invert an ordering by accident and a sort agrees across the seam.
2026 assert!(Contrast::Low < Contrast::Standard);
2027 assert!(Contrast::Standard < Contrast::High);
2028 }
2029
2030 #[test]
2031 fn the_groups_and_badges_have_one_spelling_each() {
2032 // The whole argument for these living here: three renderers picking
2033 // their own is one picker reading three ways.
2034 assert_eq!(ThemeVariant::Light.heading(), "Light");
2035 assert_eq!(ThemeVariant::Dark.heading(), "Dark");
2036 assert_eq!(ThemeVariant::HighContrast.heading(), "High Contrast");
2037
2038 assert_eq!(ThemeVariant::HighContrast.as_str(), "high-contrast");
2039
2040 assert_eq!(Contrast::High.badge(), "AA");
2041 assert_eq!(Contrast::Standard.badge(), "OK");
2042 assert_eq!(Contrast::Low.badge(), "low");
2043 }
2044
2045 #[test]
2046 fn the_variant_spelling_matches_the_theme_file_s_own() {
2047 // The seam this enum is duplicated across. `makeover::parse_meta` reads
2048 // `meta.variant` as one of these three strings; a rename on either side
2049 // that does not move together silently regroups every picker.
2050 for (variant, spelling) in [
2051 (ThemeVariant::Light, "light"),
2052 (ThemeVariant::Dark, "dark"),
2053 (ThemeVariant::HighContrast, "high-contrast"),
2054 ] {
2055 assert_eq!(variant.as_str(), spelling);
2056 assert_eq!(variant.to_string(), spelling);
2057 }
2058 }
2059