Skip to main content

max / makeover-layout

Move lib.rs's tests to the sibling the hygiene pass names The root is a facade of `pub use` lines. After the thirteen-module split it was 261 lines of re-exports carrying 2,053 lines of tests, which made the declaration the smaller half of the file that declares it. The module moves whole to `src/tests.rs`; `use super::` resolves to the crate root either way. 144 test attributes before, 144 after. Two intra-doc links did not resolve, and `cargo doc -D warnings` had never been run here to say so. `Choice::of` does not exist: `FacetValue::of` is the analogue of `Choice::plain`, both being `Self::new(value, value)`. `Cells` does not exist either; the table vocabulary a diff line rides on is `Column`, which `lib.rs` already spells `[`Table`](crate::Column)` in its own header.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01WFBzMprSmNCfvdj2cGZyka
Author: Max Johnson <me@maxj.phd> · 2026-09-05 13:30 UTC
Signed with PGP, not checked
Commit: bb2b4e93df305a7e4214bee6130f245f50ddbbdc
Parent: 43756d0
4 files changed, +506 insertions, -499 deletions
M src/facet.rs +1 -1
@@ -218,7 +218,7 @@
218 218
219 219 /// A value whose identifier is also what the user reads.
220 220 ///
221 - /// [`Choice::of`]'s convenience, and it is the flat case: a type or a tier
221 + /// [`Choice::plain`]'s convenience, and it is the flat case: a type or a tier
222 222 /// is its own name, and only a tree needs a path that is not one.
223 223 #[must_use]
224 224 pub const fn of(value: &'a str) -> Self {
M src/lib.rs -497
@@ -258,2057 +258,4 @@
258 258 pub use track::{Placement, Span, Track, Unit};
259 259
260 260 #[cfg(test)]
261 - mod tests {
262 - use super::{Change, Intent as _, Syntax};
263 -
264 - /// Every name a caller is told to use, reached through the root path.
265 - ///
266 - /// The thirteen modules are private, so a member left out of the facade
267 - /// stays reachable inside the crate and vanishes from the API outside it,
268 - /// which is a silent semver break. This is where that fails instead.
269 - #[test]
270 - fn the_names_a_caller_is_told_to_use_are_exported_from_the_root() {
271 - #[allow(unused_imports)]
272 - use crate::{
273 - Accepted, Act, Arrangement, Awaiting, Bevel, Candidate, CellPart, Change, Choice,
274 - Column, Contrast, Curve, DATE_FORMAT, DATETIME_FORMAT, Depth, Edge, Extent, Facet,
275 - FacetValue, Fallback, Family, Field, FieldKind, Figure, Fill, Fit, Flow, Heading,
276 - Intent, Loading, Measure, Meter, Nesting, Notice, Paging, Placement, Priority,
277 - Readiness, Region, RowPart, Selecting, Selector, Share, Showing, Sort, Span, Standing,
278 - State, Syntax, ThemeChoice, ThemeVariant, Token, Tone, Track, Unit, Width, Window,
279 - };
280 - }
281 -
282 - /// A run nothing claimed is a real answer rather than a missing one, which
283 - /// is why this is not an `Option<Syntax>`: a lexer that ran and found
284 - /// ordinary code has said something.
285 - #[test]
286 - fn an_unclassified_run_is_plain_and_plain_is_the_default() {
287 - assert_eq!(Syntax::default(), Syntax::Plain);
288 - assert_eq!(Syntax::Plain.name(), "plain");
289 - }
290 -
291 - /// Every class has a stable lowercase name, and no two share one. The
292 - /// spelling is named here rather than agreed per renderer, which is what
293 - /// stops one calling it `str` and the next `string`.
294 - #[test]
295 - fn every_syntax_class_has_its_own_spelling() {
296 - let all = [
297 - Syntax::Plain,
298 - Syntax::Keyword,
299 - Syntax::String,
300 - Syntax::Comment,
301 - Syntax::Constant,
302 - Syntax::Entity,
303 - Syntax::Variable,
304 - Syntax::Support,
305 - ];
306 - let mut seen = std::collections::BTreeSet::new();
307 - for class in all {
308 - assert!(!class.name().is_empty(), "{class:?} has no spelling");
309 - assert!(seen.insert(class.name()), "{class:?} shares a spelling");
310 - }
311 - assert_eq!(seen.len(), all.len());
312 - }
313 -
314 - /// Context is a state and not an absence, so an unchanged line is
315 - /// distinguishable from a line nobody marked. `Syntax::Plain`'s argument.
316 - #[test]
317 - fn an_unmarked_diff_line_is_context() {
318 - assert_eq!(Change::default(), Change::Context);
319 - assert_eq!(Change::Context.token(), "content");
320 - }
321 -
322 - /// The two sides reuse the status axis rather than minting a palette. An
323 - /// app holding them fixed across themes says so in its own sheet.
324 - #[test]
325 - fn the_two_sides_of_a_change_take_the_status_intents() {
326 - assert_eq!(Change::Added.token(), "success");
327 - assert_eq!(Change::Removed.token(), "danger");
328 - }
329 -
330 - /// The concept is named here and its magnitude is not, which is
331 - /// `Awaiting`'s split and is why a renderer can disagree with another
332 - /// about what a level is worth without either of them being wrong.
333 - #[test]
334 - fn nesting_says_how_deep_and_not_how_wide() {
335 - assert_eq!(Nesting::default(), Nesting::top());
336 - assert_eq!(Nesting::top().level, 0);
337 - assert!(!Nesting::top().is_nested());
338 -
339 - let under = Nesting::at(2);
340 - assert_eq!(under.level, 2);
341 - assert!(under.is_nested());
342 -
343 - // Ordered, so a renderer walking a flat list of rows can compare two
344 - // levels rather than reaching into the field.
345 - assert!(Nesting::top() < under);
346 - }
347 -
348 - use super::*;
349 -
350 - #[test]
351 - fn a_markdown_field_is_multiline_and_offers_nothing() {
352 - // The editing counterpart of markdown prose is still text: every host
353 - // can draw it, which is the whole reason the kind was addable.
354 - assert!(FieldKind::Rich.multiline());
355 - assert!(FieldKind::Textarea.multiline());
356 - assert!(!FieldKind::Text.multiline());
357 - // It is not a chooser and not a moment.
358 - assert!(!FieldKind::Rich.offers_options());
359 - assert!(!FieldKind::Rich.temporal());
360 - assert!(FieldKind::Rich.visible());
361 - }
362 -
363 - #[test]
364 - fn only_the_numeric_kinds_are_measurable() {
365 - assert!(FieldKind::Number.measurable());
366 - assert!(FieldKind::Range.measurable());
367 - // An axis is measured in something and both its ends are in it, so the
368 - // unit is read once for the pair rather than per end.
369 - assert!(FieldKind::Interval.measurable());
370 - // A date is ordered and is not a quantity with a unit to choose: its
371 - // unit is fixed by the kind, so saying one would restate `kind`.
372 - for kind in [
373 - FieldKind::Text,
374 - FieldKind::Date,
375 - FieldKind::DateTime,
376 - FieldKind::Select,
377 - FieldKind::Checkbox,
378 - FieldKind::File,
379 - ] {
380 - assert!(!kind.measurable(), "{kind:?}");
381 - }
382 - }
383 -
384 - #[test]
385 - fn a_field_carries_no_unit_until_one_is_given() {
386 - // Additive: absent is what every field described before 0.33.0 meant.
387 - let plain = Field::new(FieldKind::Number, "attack", "Attack");
388 - assert_eq!(plain.unit, None);
389 - let measured = Field {
390 - unit: Some("s"),
391 - ..Field::range("attack", "Attack", "0.001", "5")
392 - };
393 - assert_eq!(measured.unit, Some("s"));
394 - assert!(measured.kind.measurable());
395 - }
396 -
397 - #[test]
398 - fn an_interval_states_both_ends_names() {
399 - // Stated rather than derived: the two measured sites disagree about
400 - // affix order, so a rule here would rename one of them.
401 - let suffixed = Field::interval("bpm_min", "bpm_max", "BPM");
402 - assert_eq!(suffixed.name, "bpm_min");
403 - assert_eq!(suffixed.upper_name, Some("bpm_max"));
404 - let prefixed = Field::interval("min_price", "max_price", "Price");
405 - assert_eq!(prefixed.name, "min_price");
406 - assert_eq!(prefixed.upper_name, Some("max_price"));
407 - assert_eq!(suffixed.kind, FieldKind::Interval);
408 - }
409 -
410 - #[test]
411 - fn every_other_kind_has_no_upper_end() {
412 - // Additive: absent is what every field described before 0.34.0 meant.
413 - for kind in [FieldKind::Text, FieldKind::Number, FieldKind::Range] {
414 - assert_eq!(Field::new(kind, "n", "N").upper_name, None, "{kind:?}");
415 - }
416 - assert_eq!(Field::range("t", "T", "0", "1").upper_name, None);
417 - }
418 -
419 - #[test]
420 - fn an_interval_owes_no_bounds_and_takes_the_axis_facts_once() {
421 - // A range's bounds are the control's extent and are owed; an interval's
422 - // are a rule on each end, which is Number's arrangement.
423 - let plain = Field::interval("bpm_min", "bpm_max", "BPM");
424 - assert!(!plain.bounded());
425 - let axis = Field {
426 - min: Some("0"),
427 - max: Some("300"),
428 - step: Some("1"),
429 - unit: Some("BPM"),
430 - ..Field::interval("bpm_min", "bpm_max", "BPM")
431 - };
432 - assert!(axis.bounded());
433 - assert_eq!(axis.unit, Some("BPM"));
434 - // Nothing here checks the crossing rule, exactly as nothing checks
435 - // `min` for a number: the description carries constraints and whoever
436 - // validated decides a value is wrong.
437 - assert!(axis.error.is_none());
438 - }
439 -
440 - #[test]
441 - fn only_a_subtree_prunes_and_only_the_listing_modes_offer_values() {
442 - // Excluding a value from a flat facet is the same fact as not picking
443 - // it, so the affordance exists in exactly one mode.
444 - assert!(Selecting::Subtree.prunes());
445 - for mode in [
446 - Selecting::OneOf,
447 - Selecting::AnyOf,
448 - Selecting::Range,
449 - Selecting::Text,
450 - ] {
451 - assert!(!mode.prunes(), "{mode:?}");
452 - }
453 - // Text and Range answer with something that is not one of a set.
454 - assert!(!Selecting::Text.offers_values());
455 - assert!(!Selecting::Range.offers_values());
456 - assert!(Selecting::OneOf.offers_values());
457 - assert!(Selecting::AnyOf.accumulates());
458 - assert!(!Selecting::OneOf.accumulates());
459 - }
460 -
461 - #[test]
462 - fn an_inherited_value_is_in_force_without_having_been_picked() {
463 - // The distinction a bool cannot hold, and the reason Standing has four
464 - // members: a child under a taken parent narrows the set, and clearing
465 - // it clears nothing.
466 - assert!(Standing::Inherited.in_force());
467 - assert!(!Standing::Inherited.is_picked());
468 - assert!(Standing::Taken.in_force());
469 - assert!(Standing::Taken.is_picked());
470 - // A prune is a decision that takes the value out.
471 - assert!(Standing::Pruned.is_picked());
472 - assert!(!Standing::Pruned.in_force());
473 - assert!(!Standing::Open.is_picked());
474 - assert!(!Standing::Open.in_force());
475 - // A pruned branch still answers a press, so it may not read as inert.
476 - assert_ne!(Standing::Pruned.intent(), "content-muted");
477 - }
478 -
479 - #[test]
480 - fn a_facet_is_engaged_by_a_decision_and_not_by_an_inherited_value() {
481 - let inherited = [
482 - FacetValue::of("music")
483 - .standing(Standing::Taken)
484 - .at(Nesting::at(0), true),
485 - FacetValue::new("music/synths", "synths")
486 - .standing(Standing::Inherited)
487 - .at(Nesting::at(1), false),
488 - ];
489 - let facet = Facet::new("Tag", Selecting::Subtree, &inherited);
490 - assert!(facet.engaged());
491 - // The gutter an indenting renderer reserves before its first paint.
492 - assert_eq!(facet.reach(), 1);
493 -
494 - let untouched = [
495 - FacetValue::of("music").at(Nesting::at(0), true),
496 - FacetValue::new("music/synths", "synths")
497 - .standing(Standing::Inherited)
498 - .at(Nesting::at(1), false),
499 - ];
500 - // Inherited alone is something further up doing the narrowing, and
501 - // there is nothing further up here.
502 - assert!(!Facet::new("Tag", Selecting::Subtree, &untouched).engaged());
503 -
504 - // A text facet lists nothing, so it is flat and never reads as engaged
505 - // from its values: the typed string is not held here.
506 - let typed = Facet::new("Search", Selecting::Text, &[]);
507 - assert!(!typed.engaged());
508 - assert_eq!(typed.reach(), 0);
509 - }
510 -
511 - #[test]
512 - fn a_count_is_absent_rather_than_zero_when_it_was_not_measured() {
513 - // Awaiting::amount's rule in a second place: a written zero reads as
514 - // "none of them", which is a different claim from "not counted".
515 - assert_eq!(FacetValue::of("Ambient").count, None);
516 - assert_eq!(FacetValue::of("Ambient").counted(0).count, Some(0));
517 - }
518 -
519 - #[test]
520 - fn one_kind_takes_files_and_the_two_file_members_are_its_alone() {
521 - assert!(FieldKind::File.takes_files());
522 - for kind in [
523 - FieldKind::Text,
524 - FieldKind::Textarea,
525 - FieldKind::Rich,
526 - FieldKind::Select,
527 - FieldKind::Checkbox,
528 - FieldKind::Hidden,
529 - ] {
530 - assert!(!kind.takes_files());
531 - }
532 - // The default is a field that takes any one file, which is what an
533 - // input with no accept and no multiple already is.
534 - let plain = Field::new(FieldKind::File, "cover", "Cover");
535 - assert!(plain.accept.is_empty());
536 - assert!(!plain.multiple);
537 - }
538 -
539 - #[test]
540 - fn an_accept_list_says_which_disclosure_and_a_suffix_says_none() {
541 - // The three shapes are the MNW server's own three, and the family is
542 - // the question a renderer asks before it keeps room for a preview.
543 - assert_eq!(
544 - Accepted::Family(Family::Image).family(),
545 - Some(Family::Image)
546 - );
547 - assert_eq!(Accepted::Type("image/jpeg").family(), Some(Family::Image));
548 - assert_eq!(Accepted::Type("audio/flac").family(), Some(Family::Audio));
549 - assert_eq!(
550 - Accepted::Type("video/quicktime").family(),
551 - Some(Family::Video)
552 - );
553 - // A media type outside the three families names none, and neither does
554 - // a suffix. `.mp3` is audio in fact and this crate will not infer it:
555 - // the table that said so would rot.
556 - assert_eq!(Accepted::Type("text/csv").family(), None);
557 - assert_eq!(Accepted::Suffix(".mp3").family(), None);
558 - assert_eq!(Accepted::Suffix(".tar.gz").family(), None);
559 - // Media types are case-insensitive and half the tree writes them
560 - // lowercase by habit rather than by rule.
561 - assert_eq!(Accepted::Type("IMAGE/PNG").family(), Some(Family::Image));
562 - }
563 -
564 - #[test]
565 - fn every_accepted_entry_has_one_spelling_a_host_can_write() {
566 - assert_eq!(Accepted::Family(Family::Image).as_str(), "image/*");
567 - assert_eq!(Accepted::Family(Family::Audio).as_str(), "audio/*");
568 - assert_eq!(Accepted::Family(Family::Video).as_str(), "video/*");
569 - assert_eq!(Accepted::Type("text/csv").as_str(), "text/csv");
570 - assert_eq!(Accepted::Suffix(".tar.gz").as_str(), ".tar.gz");
571 - }
572 -
573 - #[test]
574 - fn a_list_accepting_two_families_still_has_a_disclosure_to_offer() {
575 - // The measured dropzone: `accept="image/*,video/*"`. There is no single
576 - // family to return and there is still a preview to keep room for, which
577 - // is why the question is asked of the list rather than of one entry.
578 - const MEDIA: &[Accepted<'_>] = &[
579 - Accepted::Family(Family::Image),
580 - Accepted::Family(Family::Video),
581 - ];
582 - assert!(Field::upload("media", "Media", MEDIA).accepts_media());
583 - // An installer's suffix list wants no disclosure, which is the measured
584 - // case rather than a hypothetical one.
585 - const BUILDS: &[Accepted<'_>] = &[Accepted::Suffix(".zip"), Accepted::Suffix(".dmg")];
586 - assert!(!Field::upload("build", "Build", BUILDS).accepts_media());
587 - // And a field that takes anything says so by listing nothing.
588 - assert!(!Field::upload("any", "File", &[]).accepts_media());
589 - }
590 -
591 - #[test]
592 - fn an_upload_carries_its_list_and_takes_one_file_until_it_says_otherwise() {
593 - const IMAGES: &[Accepted<'_>] = &[
594 - Accepted::Type("image/jpeg"),
595 - Accepted::Type("image/png"),
596 - Accepted::Type("image/webp"),
597 - ];
598 - let avatar = Field::upload("avatar", "Avatar", IMAGES);
599 - assert_eq!(avatar.kind, FieldKind::File);
600 - assert_eq!(avatar.accept, IMAGES);
601 - assert!(!avatar.multiple);
602 - let several = Field {
603 - multiple: true,
604 - ..avatar
605 - };
606 - assert!(several.multiple);
607 - }
608 -
609 - #[test]
610 - fn the_four_readiness_states_are_one_axis_and_only_one_shows_content() {
611 - // Mutually exclusive is the test for one enum against several fields: a
612 - // region shows its content, or that it is coming, or that there is none,
613 - // or that it broke. Never two.
614 - assert!(Readiness::Ready.shows_content());
615 - for state in [Readiness::Pending, Readiness::Empty, Readiness::Failed] {
616 - assert!(!state.shows_content());
617 - }
618 - }
619 -
620 - #[test]
621 - fn a_region_shows_all_of_its_children_unless_it_says_otherwise() {
622 - // The default is the behaviour every region had before this member
623 - // existed, which is what keeps it additive: a description written
624 - // against 0.22.0 says the same thing under 0.23.0.
625 - assert_eq!(Showing::default(), Showing::All);
626 - assert!(!Showing::All.selective());
627 - }
628 -
629 - #[test]
630 - fn only_a_disclosure_can_show_nothing() {
631 - // The two derived idioms differ in one respect and this is it. A
632 - // carousel's row moves between frames and never reaches empty; a
633 - // disclosure's summary line is the same control wearing its closed
634 - // state, so a renderer has to know which it is drawing.
635 - assert!(Showing::AtMostOne.dismissible());
636 - assert!(!Showing::One.dismissible());
637 - assert!(!Showing::All.dismissible());
638 -
639 - // Both are selective, though. Deriving chrome is one question and
640 - // whether that chrome closes is another.
641 - assert!(Showing::One.selective());
642 - assert!(Showing::AtMostOne.selective());
643 - }
644 -
645 - #[test]
646 - fn an_empty_region_is_not_a_broken_one() {
647 - // An empty list is the normal state of a new install. Drawing it in a
648 - // danger tone reports a fault where there is none, and this is the one
649 - // place the distinction is carried.
650 - assert_eq!(Readiness::Empty.tone(), Tone::Neutral);
651 - assert_eq!(Readiness::Failed.tone(), Tone::Danger);
652 - assert_eq!(Readiness::Pending.tone(), Tone::Neutral);
653 - }
654 -
655 - #[test]
656 - fn a_column_can_be_sorted_without_being_sortable() {
657 - // Both combinations mean something, which is why the two fields are
658 - // independent rather than one implying the other. A list ordered by a
659 - // key the user cannot change is a real thing with a caret worth drawing.
660 - let fixed = Column {
661 - sorted: Some(Sort::Descending),
662 - ..Column::new("Created")
663 - };
664 -
665 - assert!(!fixed.sortable);
666 - assert_eq!(fixed.sorted.map(Sort::as_str), Some("descending"));
667 -
668 - let offered = Column {
669 - sortable: true,
670 - ..Column::new("Name")
671 - };
672 - assert_eq!(offered.sorted, None);
673 - }
674 -
675 - #[test]
676 - fn a_direction_flips_and_says_what_it_is() {
677 - assert_eq!(Sort::Ascending.reversed(), Sort::Descending);
678 - assert_eq!(Sort::Descending.reversed().reversed(), Sort::Descending);
679 - assert_eq!(Sort::Ascending.as_str(), "ascending");
680 - }
681 -
682 - #[test]
683 - fn a_direction_carries_its_caret_and_the_two_are_not_the_same_glyph() {
684 - // The spelling every renderer reads, so that agreeing is composition
685 - // rather than three files happening to hold the same literal.
686 - assert_eq!(Sort::Ascending.glyph(), "\u{25B2}");
687 - assert_eq!(Sort::Descending.glyph(), "\u{25BC}");
688 - assert_ne!(Sort::Ascending.glyph(), Sort::Descending.glyph());
689 - // Bare. The gap is the renderer's, and a space here would be a second
690 - // one wherever a renderer already carries its own.
691 - for d in [Sort::Ascending, Sort::Descending] {
692 - assert_eq!(d.glyph().trim(), d.glyph());
693 - }
694 - }
695 -
696 - #[test]
697 - fn a_figure_carries_its_tone_because_no_renderer_can_derive_it() {
698 - // Three of goingson's five sites tone the figure by their own means, so
699 - // tone is carried at every site that needs it and derived at none. The
700 - // same reasoning `Meter` reached, from a different direction.
701 - let streak = Figure::new("0", "Current Streak").tone(Tone::Warning);
702 - assert_eq!(streak.tone, Tone::Warning);
703 - assert_eq!(Figure::new("17", "Total").tone, Tone::Neutral);
704 - }
705 -
706 - #[test]
707 - fn a_figures_change_is_the_toned_part_and_is_absent_by_default() {
708 - // 0.13.0. The MNW server's stat card is a label, a value and a delta,
709 - // across four screens, and the delta is what reads as good or bad. Tone
710 - // had no consumer before this: the figure itself is an ordinary fact.
711 - let views = Figure::new("1,204", "Views")
712 - .change("+12.5%")
713 - .tone(Tone::Success);
714 - assert_eq!(views.change, Some("+12.5%"));
715 - assert_eq!(views.tone, Tone::Success);
716 -
717 - // A figure with nothing to compare against says so by having no change,
718 - // rather than by carrying an empty string a renderer has to test for.
719 - assert_eq!(Figure::new("3.1%", "Conversion").change, None);
720 - }
721 -
722 - #[test]
723 - fn a_figures_value_is_text_because_only_the_app_knows_what_it_is() {
724 - // "84%", "12/30", "3d". A figure is whatever the app computed, already
725 - // formatted, and that is the line between this and `Meter`: a meter is
726 - // a proportion a renderer draws, a figure is a fact it sets in type.
727 - for value in ["84%", "12/30", "3d"] {
728 - assert_eq!(Figure::new(value, "Rate").value, value);
729 - }
730 - }
731 -
732 - #[test]
733 - fn a_proportion_is_a_row_part_and_takes_no_intent_of_its_own() {
734 - // The meter carries the tone, so a part-level intent underneath would
735 - // fight it. Same answer `Tokens` needed, for the same reason.
736 - assert_eq!(RowPart::Proportion.intent(), RowPart::Tokens.intent());
737 - }
738 -
739 - #[test]
740 - fn a_file_field_is_drawn_and_offers_no_options() {
741 - // It is a control the user operates, unlike `Hidden`, and it does not
742 - // pick from a list the description carries, unlike `Select`.
743 - assert!(FieldKind::File.visible());
744 - assert!(!FieldKind::File.offers_options());
745 - assert!(!FieldKind::File.confidential());
746 - }
747 -
748 - #[test]
749 - fn a_constraint_is_a_fact_about_the_question_and_not_a_verdict() {
750 - // The whole model: the description carries the rule, the renderer emits
751 - // its host's idiom, and `error` is what arrives back when someone
752 - // validated. Nothing here decides a value is wrong.
753 - let field = Field {
754 - max_length: Some(100),
755 - min: Some("1"),
756 - max: Some("240"),
757 - required: true,
Lines truncated
M src/text.rs +5 -1
@@ -1,5 +1,9 @@
1 1 use crate::Intent;
2 2
3 + // Names this module's prose links to, resolved for rustdoc.
4 + #[allow(unused_imports)]
5 + use crate::Column;
6 +
3 7 /// What a run of source code is, once something has classified it.
4 8 ///
5 9 /// The description carries the classification and never the source, which is
@@ -101,7 +105,7 @@
101 105 ///
102 106 /// The other half of decision `19d7602d`. A diff is a table of lines and the
103 107 /// only thing the vocabulary was missing was a way for a line to say which side
104 - /// of the change it is on, so this rides on [`Cells`] rather than arriving as a
108 + /// of the change it is on, so this rides on [`Column`] rather than arriving as a
105 109 /// `Node::Diff` carrying git's data model.
106 110 ///
107 111 /// # Why three and not two
A src/tests.rs +500
@@ -1,0 +1,2058 @@
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"),
Lines truncated