//! The crate's own tests, in the file the crate's `mod tests;` names. //! //! Moved out of `lib.rs` whole: the root is a facade of `pub use` lines, and //! 2,000 lines of tests under 250 lines of re-exports made the facade the //! smaller half of the file that declares it. use super::{Change, Intent as _, Syntax}; /// Every name a caller is told to use, reached through the root path. /// /// The thirteen modules are private, so a member left out of the facade /// stays reachable inside the crate and vanishes from the API outside it, /// which is a silent semver break. This is where that fails instead. #[test] fn the_names_a_caller_is_told_to_use_are_exported_from_the_root() { #[allow(unused_imports)] use crate::{ Accepted, Act, Arrangement, Awaiting, Bevel, Candidate, CellPart, Change, Choice, Column, Contrast, Curve, DATE_FORMAT, DATETIME_FORMAT, Depth, Edge, Extent, Facet, FacetValue, Fallback, Family, Field, FieldKind, Figure, Fill, Fit, Flow, Heading, Intent, Loading, Measure, Meter, Nesting, Notice, Paging, Placement, Priority, Readiness, Region, RowPart, Selecting, Selector, Share, Showing, Sort, Span, Standing, State, Syntax, ThemeChoice, ThemeVariant, Token, Tone, Track, Unit, Width, Window, }; } /// A run nothing claimed is a real answer rather than a missing one, which /// is why this is not an `Option`: a lexer that ran and found /// ordinary code has said something. #[test] fn an_unclassified_run_is_plain_and_plain_is_the_default() { assert_eq!(Syntax::default(), Syntax::Plain); assert_eq!(Syntax::Plain.name(), "plain"); } /// Every class has a stable lowercase name, and no two share one. The /// spelling is named here rather than agreed per renderer, which is what /// stops one calling it `str` and the next `string`. #[test] fn every_syntax_class_has_its_own_spelling() { let all = [ Syntax::Plain, Syntax::Keyword, Syntax::String, Syntax::Comment, Syntax::Constant, Syntax::Entity, Syntax::Variable, Syntax::Support, ]; let mut seen = std::collections::BTreeSet::new(); for class in all { assert!(!class.name().is_empty(), "{class:?} has no spelling"); assert!(seen.insert(class.name()), "{class:?} shares a spelling"); } assert_eq!(seen.len(), all.len()); } /// Context is a state and not an absence, so an unchanged line is /// distinguishable from a line nobody marked. `Syntax::Plain`'s argument. #[test] fn an_unmarked_diff_line_is_context() { assert_eq!(Change::default(), Change::Context); assert_eq!(Change::Context.token(), "content"); } /// The two sides reuse the status axis rather than minting a palette. An /// app holding them fixed across themes says so in its own sheet. #[test] fn the_two_sides_of_a_change_take_the_status_intents() { assert_eq!(Change::Added.token(), "success"); assert_eq!(Change::Removed.token(), "danger"); } /// The concept is named here and its magnitude is not, which is /// `Awaiting`'s split and is why a renderer can disagree with another /// about what a level is worth without either of them being wrong. #[test] fn nesting_says_how_deep_and_not_how_wide() { assert_eq!(Nesting::default(), Nesting::top()); assert_eq!(Nesting::top().level, 0); assert!(!Nesting::top().is_nested()); let under = Nesting::at(2); assert_eq!(under.level, 2); assert!(under.is_nested()); // Ordered, so a renderer walking a flat list of rows can compare two // levels rather than reaching into the field. assert!(Nesting::top() < under); } use super::*; #[test] fn a_markdown_field_is_multiline_and_offers_nothing() { // The editing counterpart of markdown prose is still text: every host // can draw it, which is the whole reason the kind was addable. assert!(FieldKind::Rich.multiline()); assert!(FieldKind::Textarea.multiline()); assert!(!FieldKind::Text.multiline()); // It is not a chooser and not a moment. assert!(!FieldKind::Rich.offers_options()); assert!(!FieldKind::Rich.temporal()); assert!(FieldKind::Rich.visible()); } #[test] fn only_the_numeric_kinds_are_measurable() { assert!(FieldKind::Number.measurable()); assert!(FieldKind::Range.measurable()); // An axis is measured in something and both its ends are in it, so the // unit is read once for the pair rather than per end. assert!(FieldKind::Interval.measurable()); // A date is ordered and is not a quantity with a unit to choose: its // unit is fixed by the kind, so saying one would restate `kind`. for kind in [ FieldKind::Text, FieldKind::Date, FieldKind::DateTime, FieldKind::Select, FieldKind::Checkbox, FieldKind::File, ] { assert!(!kind.measurable(), "{kind:?}"); } } #[test] fn a_field_carries_no_unit_until_one_is_given() { // Additive: absent is what every field described before 0.33.0 meant. let plain = Field::new(FieldKind::Number, "attack", "Attack"); assert_eq!(plain.unit, None); let measured = Field { unit: Some("s"), ..Field::range("attack", "Attack", "0.001", "5") }; assert_eq!(measured.unit, Some("s")); assert!(measured.kind.measurable()); } #[test] fn an_interval_states_both_ends_names() { // Stated rather than derived: the two measured sites disagree about // affix order, so a rule here would rename one of them. let suffixed = Field::interval("bpm_min", "bpm_max", "BPM"); assert_eq!(suffixed.name, "bpm_min"); assert_eq!(suffixed.upper_name, Some("bpm_max")); let prefixed = Field::interval("min_price", "max_price", "Price"); assert_eq!(prefixed.name, "min_price"); assert_eq!(prefixed.upper_name, Some("max_price")); assert_eq!(suffixed.kind, FieldKind::Interval); } #[test] fn every_other_kind_has_no_upper_end() { // Additive: absent is what every field described before 0.34.0 meant. for kind in [FieldKind::Text, FieldKind::Number, FieldKind::Range] { assert_eq!(Field::new(kind, "n", "N").upper_name, None, "{kind:?}"); } assert_eq!(Field::range("t", "T", "0", "1").upper_name, None); } #[test] fn an_interval_owes_no_bounds_and_takes_the_axis_facts_once() { // A range's bounds are the control's extent and are owed; an interval's // are a rule on each end, which is Number's arrangement. let plain = Field::interval("bpm_min", "bpm_max", "BPM"); assert!(!plain.bounded()); let axis = Field { min: Some("0"), max: Some("300"), step: Some("1"), unit: Some("BPM"), ..Field::interval("bpm_min", "bpm_max", "BPM") }; assert!(axis.bounded()); assert_eq!(axis.unit, Some("BPM")); // Nothing here checks the crossing rule, exactly as nothing checks // `min` for a number: the description carries constraints and whoever // validated decides a value is wrong. assert!(axis.error.is_none()); } #[test] fn only_a_subtree_prunes_and_only_the_listing_modes_offer_values() { // Excluding a value from a flat facet is the same fact as not picking // it, so the affordance exists in exactly one mode. assert!(Selecting::Subtree.prunes()); for mode in [ Selecting::OneOf, Selecting::AnyOf, Selecting::Range, Selecting::Text, ] { assert!(!mode.prunes(), "{mode:?}"); } // Text and Range answer with something that is not one of a set. assert!(!Selecting::Text.offers_values()); assert!(!Selecting::Range.offers_values()); assert!(Selecting::OneOf.offers_values()); assert!(Selecting::AnyOf.accumulates()); assert!(!Selecting::OneOf.accumulates()); } #[test] fn an_inherited_value_is_in_force_without_having_been_picked() { // The distinction a bool cannot hold, and the reason Standing has four // members: a child under a taken parent narrows the set, and clearing // it clears nothing. assert!(Standing::Inherited.in_force()); assert!(!Standing::Inherited.is_picked()); assert!(Standing::Taken.in_force()); assert!(Standing::Taken.is_picked()); // A prune is a decision that takes the value out. assert!(Standing::Pruned.is_picked()); assert!(!Standing::Pruned.in_force()); assert!(!Standing::Open.is_picked()); assert!(!Standing::Open.in_force()); // A pruned branch still answers a press, so it may not read as inert. assert_ne!(Standing::Pruned.intent(), "content-muted"); } #[test] fn a_facet_is_engaged_by_a_decision_and_not_by_an_inherited_value() { let inherited = [ FacetValue::of("music") .standing(Standing::Taken) .at(Nesting::at(0), true), FacetValue::new("music/synths", "synths") .standing(Standing::Inherited) .at(Nesting::at(1), false), ]; let facet = Facet::new("Tag", Selecting::Subtree, &inherited); assert!(facet.engaged()); // The gutter an indenting renderer reserves before its first paint. assert_eq!(facet.reach(), 1); let untouched = [ FacetValue::of("music").at(Nesting::at(0), true), FacetValue::new("music/synths", "synths") .standing(Standing::Inherited) .at(Nesting::at(1), false), ]; // Inherited alone is something further up doing the narrowing, and // there is nothing further up here. assert!(!Facet::new("Tag", Selecting::Subtree, &untouched).engaged()); // A text facet lists nothing, so it is flat and never reads as engaged // from its values: the typed string is not held here. let typed = Facet::new("Search", Selecting::Text, &[]); assert!(!typed.engaged()); assert_eq!(typed.reach(), 0); } #[test] fn a_count_is_absent_rather_than_zero_when_it_was_not_measured() { // Awaiting::amount's rule in a second place: a written zero reads as // "none of them", which is a different claim from "not counted". assert_eq!(FacetValue::of("Ambient").count, None); assert_eq!(FacetValue::of("Ambient").counted(0).count, Some(0)); } #[test] fn one_kind_takes_files_and_the_two_file_members_are_its_alone() { assert!(FieldKind::File.takes_files()); for kind in [ FieldKind::Text, FieldKind::Textarea, FieldKind::Rich, FieldKind::Select, FieldKind::Checkbox, FieldKind::Hidden, ] { assert!(!kind.takes_files()); } // The default is a field that takes any one file, which is what an // input with no accept and no multiple already is. let plain = Field::new(FieldKind::File, "cover", "Cover"); assert!(plain.accept.is_empty()); assert!(!plain.multiple); } #[test] fn an_accept_list_says_which_disclosure_and_a_suffix_says_none() { // The three shapes are the MNW server's own three, and the family is // the question a renderer asks before it keeps room for a preview. assert_eq!( Accepted::Family(Family::Image).family(), Some(Family::Image) ); assert_eq!(Accepted::Type("image/jpeg").family(), Some(Family::Image)); assert_eq!(Accepted::Type("audio/flac").family(), Some(Family::Audio)); assert_eq!( Accepted::Type("video/quicktime").family(), Some(Family::Video) ); // A media type outside the three families names none, and neither does // a suffix. `.mp3` is audio in fact and this crate will not infer it: // the table that said so would rot. assert_eq!(Accepted::Type("text/csv").family(), None); assert_eq!(Accepted::Suffix(".mp3").family(), None); assert_eq!(Accepted::Suffix(".tar.gz").family(), None); // Media types are case-insensitive and half the tree writes them // lowercase by habit rather than by rule. assert_eq!(Accepted::Type("IMAGE/PNG").family(), Some(Family::Image)); } #[test] fn every_accepted_entry_has_one_spelling_a_host_can_write() { assert_eq!(Accepted::Family(Family::Image).as_str(), "image/*"); assert_eq!(Accepted::Family(Family::Audio).as_str(), "audio/*"); assert_eq!(Accepted::Family(Family::Video).as_str(), "video/*"); assert_eq!(Accepted::Type("text/csv").as_str(), "text/csv"); assert_eq!(Accepted::Suffix(".tar.gz").as_str(), ".tar.gz"); } #[test] fn a_list_accepting_two_families_still_has_a_disclosure_to_offer() { // The measured dropzone: `accept="image/*,video/*"`. There is no single // family to return and there is still a preview to keep room for, which // is why the question is asked of the list rather than of one entry. const MEDIA: &[Accepted<'_>] = &[ Accepted::Family(Family::Image), Accepted::Family(Family::Video), ]; assert!(Field::upload("media", "Media", MEDIA).accepts_media()); // An installer's suffix list wants no disclosure, which is the measured // case rather than a hypothetical one. const BUILDS: &[Accepted<'_>] = &[Accepted::Suffix(".zip"), Accepted::Suffix(".dmg")]; assert!(!Field::upload("build", "Build", BUILDS).accepts_media()); // And a field that takes anything says so by listing nothing. assert!(!Field::upload("any", "File", &[]).accepts_media()); } #[test] fn an_upload_carries_its_list_and_takes_one_file_until_it_says_otherwise() { const IMAGES: &[Accepted<'_>] = &[ Accepted::Type("image/jpeg"), Accepted::Type("image/png"), Accepted::Type("image/webp"), ]; let avatar = Field::upload("avatar", "Avatar", IMAGES); assert_eq!(avatar.kind, FieldKind::File); assert_eq!(avatar.accept, IMAGES); assert!(!avatar.multiple); let several = Field { multiple: true, ..avatar }; assert!(several.multiple); } #[test] fn the_four_readiness_states_are_one_axis_and_only_one_shows_content() { // Mutually exclusive is the test for one enum against several fields: a // region shows its content, or that it is coming, or that there is none, // or that it broke. Never two. assert!(Readiness::Ready.shows_content()); for state in [Readiness::Pending, Readiness::Empty, Readiness::Failed] { assert!(!state.shows_content()); } } #[test] fn a_region_shows_all_of_its_children_unless_it_says_otherwise() { // The default is the behaviour every region had before this member // existed, which is what keeps it additive: a description written // against 0.22.0 says the same thing under 0.23.0. assert_eq!(Showing::default(), Showing::All); assert!(!Showing::All.selective()); } #[test] fn only_a_disclosure_can_show_nothing() { // The two derived idioms differ in one respect and this is it. A // carousel's row moves between frames and never reaches empty; a // disclosure's summary line is the same control wearing its closed // state, so a renderer has to know which it is drawing. assert!(Showing::AtMostOne.dismissible()); assert!(!Showing::One.dismissible()); assert!(!Showing::All.dismissible()); // Both are selective, though. Deriving chrome is one question and // whether that chrome closes is another. assert!(Showing::One.selective()); assert!(Showing::AtMostOne.selective()); } #[test] fn an_empty_region_is_not_a_broken_one() { // An empty list is the normal state of a new install. Drawing it in a // danger tone reports a fault where there is none, and this is the one // place the distinction is carried. assert_eq!(Readiness::Empty.tone(), Tone::Neutral); assert_eq!(Readiness::Failed.tone(), Tone::Danger); assert_eq!(Readiness::Pending.tone(), Tone::Neutral); } #[test] fn a_column_can_be_sorted_without_being_sortable() { // Both combinations mean something, which is why the two fields are // independent rather than one implying the other. A list ordered by a // key the user cannot change is a real thing with a caret worth drawing. let fixed = Column { sorted: Some(Sort::Descending), ..Column::new("Created") }; assert!(!fixed.sortable); assert_eq!(fixed.sorted.map(Sort::as_str), Some("descending")); let offered = Column { sortable: true, ..Column::new("Name") }; assert_eq!(offered.sorted, None); } #[test] fn a_direction_flips_and_says_what_it_is() { assert_eq!(Sort::Ascending.reversed(), Sort::Descending); assert_eq!(Sort::Descending.reversed().reversed(), Sort::Descending); assert_eq!(Sort::Ascending.as_str(), "ascending"); } #[test] fn a_direction_carries_its_caret_and_the_two_are_not_the_same_glyph() { // The spelling every renderer reads, so that agreeing is composition // rather than three files happening to hold the same literal. assert_eq!(Sort::Ascending.glyph(), "\u{25B2}"); assert_eq!(Sort::Descending.glyph(), "\u{25BC}"); assert_ne!(Sort::Ascending.glyph(), Sort::Descending.glyph()); // Bare. The gap is the renderer's, and a space here would be a second // one wherever a renderer already carries its own. for d in [Sort::Ascending, Sort::Descending] { assert_eq!(d.glyph().trim(), d.glyph()); } } #[test] fn a_figure_carries_its_tone_because_no_renderer_can_derive_it() { // Three of goingson's five sites tone the figure by their own means, so // tone is carried at every site that needs it and derived at none. The // same reasoning `Meter` reached, from a different direction. let streak = Figure::new("0", "Current Streak").tone(Tone::Warning); assert_eq!(streak.tone, Tone::Warning); assert_eq!(Figure::new("17", "Total").tone, Tone::Neutral); } #[test] fn a_figures_change_is_the_toned_part_and_is_absent_by_default() { // 0.13.0. The MNW server's stat card is a label, a value and a delta, // across four screens, and the delta is what reads as good or bad. Tone // had no consumer before this: the figure itself is an ordinary fact. let views = Figure::new("1,204", "Views") .change("+12.5%") .tone(Tone::Success); assert_eq!(views.change, Some("+12.5%")); assert_eq!(views.tone, Tone::Success); // A figure with nothing to compare against says so by having no change, // rather than by carrying an empty string a renderer has to test for. assert_eq!(Figure::new("3.1%", "Conversion").change, None); } #[test] fn a_figures_value_is_text_because_only_the_app_knows_what_it_is() { // "84%", "12/30", "3d". A figure is whatever the app computed, already // formatted, and that is the line between this and `Meter`: a meter is // a proportion a renderer draws, a figure is a fact it sets in type. for value in ["84%", "12/30", "3d"] { assert_eq!(Figure::new(value, "Rate").value, value); } } #[test] fn a_proportion_is_a_row_part_and_takes_no_intent_of_its_own() { // The meter carries the tone, so a part-level intent underneath would // fight it. Same answer `Tokens` needed, for the same reason. assert_eq!(RowPart::Proportion.intent(), RowPart::Tokens.intent()); } #[test] fn a_file_field_is_drawn_and_offers_no_options() { // It is a control the user operates, unlike `Hidden`, and it does not // pick from a list the description carries, unlike `Select`. assert!(FieldKind::File.visible()); assert!(!FieldKind::File.offers_options()); assert!(!FieldKind::File.confidential()); } #[test] fn a_constraint_is_a_fact_about_the_question_and_not_a_verdict() { // The whole model: the description carries the rule, the renderer emits // its host's idiom, and `error` is what arrives back when someone // validated. Nothing here decides a value is wrong. let field = Field { max_length: Some(100), min: Some("1"), max: Some("240"), required: true, ..Field::new(FieldKind::Number, "minutes", "Minutes") }; assert!(!field.invalid()); // A bound is text because it is only a number for some of the kinds // that take one. goingson has both shapes live. let when = Field { min: Some("2026-08-09T14:30"), ..Field::new(FieldKind::Text, "starts", "Starts") }; assert_eq!(when.min, Some("2026-08-09T14:30")); } #[test] fn a_meter_keeps_the_over_run_the_percentage_throws_away() { // The whole reason this is a pair. goingson's `Task::time_progress` // clamps to 100 and then carries `is_over_estimate` beside it to say // what the clamp dropped; a meter says both from one fact. let over = Meter::new(45, 30); assert_eq!(over.percent(), 100); assert!(over.overflowing()); let exact = Meter::new(30, 30); assert_eq!(exact.percent(), over.percent()); assert!(!exact.overflowing()); } #[test] fn an_empty_set_does_not_divide_by_zero() { // Sayable on purpose, so it has to be answerable. A meter over an // unloaded count is what an app actually has for a frame. let none = Meter::new(0, 0); assert_eq!(none.percent(), 0); assert!(none.is_empty()); assert!(!none.overflowing()); } #[test] fn the_ratio_survives_where_a_percentage_would_not() { // Given 43 nothing can recover "3 of 7", which is why the numbers are // carried and the label names only the noun. let m = Meter::new(3, 7).label("subtasks"); assert_eq!(m.percent(), 42); assert_eq!((m.done, m.total), (3, 7)); assert_eq!(m.label, Some("subtasks")); } #[test] fn tone_is_carried_because_no_renderer_can_derive_it() { // The same fullness means opposite things on two of goingson's bars, // and only the app knows which. let subtasks = Meter::new(9, 10).tone(Tone::Success); let estimate = Meter::new(9, 10).tone(Tone::Danger); assert_eq!(subtasks.percent(), estimate.percent()); assert_ne!(subtasks.tone, estimate.tone); // Untoned by default: a bar says nothing about status until something // says so, the same way a row is not selectable until told. assert_eq!(Meter::new(9, 10).tone, Tone::Neutral); } #[test] fn an_act_is_reachable_until_it_is_disabled() { // The one member a renderer must branch on, and since 0.19.0 the only // member there is. A stated state is not by itself a reason to stop // answering, which is the distinction `State` makes and every // hand-rolled button in the tree had to remember. assert!(!Act::new("Save").disabled()); assert!(Act::new("Save").state(State::Disabled).disabled()); } #[test] fn an_act_carries_its_key_because_a_terminal_has_nothing_else() { // No key is the ordinary case, and the webview hosts that ignore it // are why it stayed optional. assert_eq!(Act::new("Delete").key, None); let quit = Act::new("Quit").key("q").tone(Tone::Danger); assert_eq!(quit.key, Some("q")); assert_eq!(quit.tone, Tone::Danger); } #[test] fn a_meter_does_not_overflow_on_large_counts() { // done * 100 in u32 would wrap somewhere past 42 million. Counts that // size are not tasks, but a description layer that silently reports 3% // for a full bar is worse than one that is slow. let big = Meter::new(u32::MAX, u32::MAX); assert_eq!(big.percent(), 100); assert!(!big.overflowing()); } #[test] fn inset_is_raised_with_the_light_moved() { let (rl, rd) = Bevel::Raised.edges(); let (il, id) = Bevel::Inset.edges(); assert_eq!((rl, rd), (Edge::Light, Edge::Dark)); assert_eq!((il, id), (rd, rl)); } #[test] fn pressing_twice_is_a_no_op() { for b in [Bevel::Raised, Bevel::Inset] { assert_eq!(b.pressed().pressed(), b); } } #[test] fn a_raised_region_is_never_filled_with_a_recessed_surface() { // The bug this vocabulary exists to make unrepresentable. assert_eq!(Depth::Raised.fill(), Some(Fill::Raised)); assert_eq!(Depth::Raised.bevel(), Some(Bevel::Raised)); assert_eq!(Depth::Well.bevel(), Some(Bevel::Inset)); assert_ne!(Depth::Well.fill(), Depth::Raised.fill()); } #[test] fn state_is_orthogonal_to_depth() { // The reason State is its own axis and not a Depth member: a disabled // button and a disabled field are both disabled and are not the same // shape, which one shared variant could not have said. assert_eq!(Depth::Raised.fill(), Some(Fill::Raised)); assert_eq!(Depth::Well.fill(), Some(Fill::Well)); assert!(State::Disabled.suppresses_interaction()); } #[test] fn only_disabled_stops_answering() { // Kept in spirit from the version where `Focus` was the counter-example: // suppressing interaction is `Disabled`'s alone, so a member added here // later does not get to inherit it by being a state. assert!(State::Disabled.suppresses_interaction()); } #[test] fn disabled_resolves_against_an_intent_makeover_already_derives() { // No new token, so this costs no `makeover` release. assert_eq!(State::Disabled.token(), "content-muted"); } #[test] fn flat_has_neither_edge_nor_fill() { assert_eq!(Depth::Flat.bevel(), None); assert_eq!(Depth::Flat.fill(), None); } #[test] fn sunken_is_recessed_by_colour_with_no_edge() { // The one member carrying a fill without a bevel. A renderer that // assumes the two arrive together drops the fill silently, which is // exactly what makeover-webview did before 0.3.0. assert_eq!(Depth::Sunken.fill(), Some(Fill::Sunken)); assert_eq!(Depth::Sunken.bevel(), None); } #[test] fn sunken_and_flat_are_different_claims() { // Both edgeless, and only one of them needs a colour. Collapsing them // is what left an unchosen tab unsayable. assert_eq!(Depth::Flat.bevel(), Depth::Sunken.bevel()); assert_ne!(Depth::Flat.fill(), Depth::Sunken.fill()); } #[test] fn a_sunken_surface_is_not_a_well() { // Authored in opposite directions: makeover derives surface-well by // inverting against the theme's content colour, while surface-sunken is // authored and may sit darker than raised. assert_ne!(Fill::Sunken, Fill::Well); assert_eq!(Fill::Sunken.token(), "surface-sunken"); assert_eq!(Fill::Well.token(), "surface-well"); } #[test] fn every_selector_describes_both_of_its_states() { // The gap 0.3.0 closed. Before it, only `chosen` existed and the // unchosen option fell through to Flat at every renderer. for s in [Selector::Tabs, Selector::Segmented, Selector::Toggle] { assert_ne!( s.chosen(), s.unchosen(), "{s:?} cannot tell picked from unpicked" ); } } #[test] fn only_a_tab_inverts_the_other_way() { // Tabs recede so the chosen one comes forward; a segment and a toggle // stand up so the chosen one is held in. That inversion is the whole // content of "picked" once colour is deferred, and it is why the three // are not one member with a flag. assert_eq!(Selector::Tabs.unchosen(), Depth::Sunken); assert_eq!(Selector::Tabs.chosen(), Depth::Raised); for s in [Selector::Segmented, Selector::Toggle] { assert_eq!(s.unchosen(), Depth::Raised); assert_eq!(s.chosen(), Depth::Well); // Held in is what pressing produces: one appearance, two reasons. assert_eq!(s.unchosen().pressed(), s.chosen()); } } #[test] fn pressing_a_card_makes_a_well() { assert_eq!(Depth::Raised.pressed(), Depth::Well); assert_eq!( Depth::Raised.pressed().bevel(), Depth::Raised.bevel().map(Bevel::pressed) ); // Only raised regions respond to being pressed. assert_eq!(Depth::Flat.pressed(), Depth::Flat); assert_eq!(Depth::Well.pressed(), Depth::Well); // An overlay is a surface, not a control. assert_eq!(Depth::Overlay.pressed(), Depth::Overlay); } #[test] fn an_overlay_is_lifted_rather_than_edged() { // The wave-2 rule: a surface over the page takes elevation, a surface // in the page takes a bevel. Both halves come off the one Depth, so // they cannot disagree. assert_eq!(Depth::Overlay.fill(), Some(Fill::Overlay)); assert_eq!(Depth::Overlay.bevel(), None); // Three depths have no bevel and they are not the same claim. Flat has // nothing to separate from, Sunken's colour is doing the separating, // and an overlay is separated by the lift. assert_ne!(Depth::Overlay.fill(), Depth::Sunken.fill()); assert_ne!(Depth::Overlay.fill(), Depth::Flat.fill()); } #[test] fn a_note_is_neither_a_hint_nor_an_error() { // audiofiles' export Format field: choosing WAV over Original // re-encodes and drops the embedded metadata. Perfectly valid, and it // costs something. let format = Field { note: Some((Tone::Warning, "Re-encoding drops embedded BWF and iXML")), ..Field::select("format", "Format", &[]) }; assert!(!format.invalid(), "a note is not a validation failure"); assert!(format.hint.is_none()); assert!(format.error.is_none()); assert_eq!(format.note.unwrap().0, Tone::Warning); // The default is no note, so the 15 in-tree `..Field::new(..)` // literals absorb the member with no call-site edit. assert!(Field::new(FieldKind::Text, "title", "Title").note.is_none()); } #[test] fn neutral_is_content_not_muted_content() { // Neutral means "no status", and that is all it means. It answered // `content-muted` until 2026-08-27, which muted a figure's headline // number to the colour of its own caption. What makes a badge quiet // is `Token::Badge` answering no click, which lives on the renderer. assert_eq!(Tone::Neutral.token(), "content"); assert!(!Token::Badge.interactive()); assert_eq!(State::Disabled.token(), "content-muted"); } #[test] fn intents_name_makeover_tokens_and_nothing_else() { assert_eq!(Edge::Light.token(), "bevel-light"); assert_eq!(Edge::Dark.token(), "bevel-dark"); assert_eq!(Fill::Raised.token(), "surface-raised"); assert_eq!(Fill::Well.token(), "surface-well"); // No value ever leaves this crate. for t in [ Edge::Light.token(), Edge::Dark.token(), Tone::Danger.token(), Tone::Neutral.token(), State::Disabled.token(), ] { assert!(!t.starts_with('#'), "{t} looks like a value"); assert!( !t.chars().next().unwrap().is_ascii_digit(), "{t} is a value" ); } } #[test] fn a_badge_cannot_be_pressed_and_a_chip_latches() { // The one line that runs through all three apps' taxonomies. assert!(!Token::Badge.interactive()); assert!(Token::Chip { removable: false }.interactive()); assert!(Token::Chip { removable: true }.interactive()); // A badge is a label, so giving it an edge would lie about it. assert_eq!(Token::Badge.depth(false), Depth::Flat); assert_eq!(Token::Badge.depth(true), Depth::Flat); // A latched chip wears the same shape a pressed one does. let chip = Token::Chip { removable: false }; assert_eq!(chip.depth(false), Depth::Raised); assert_eq!(chip.depth(true), Depth::Raised.pressed()); } #[test] fn a_toast_and_a_banner_differ_in_more_than_placement() { assert!(Notice::Toast.transient()); assert!(!Notice::Banner.transient()); // A toast floats above the page; a banner rests in the flow. assert_eq!(Notice::Toast.fill(), Fill::Overlay); assert_eq!(Notice::Banner.fill(), Fill::Raised); } #[test] fn emphasis_falls_off_down_the_row() { // `revealed_on_hover` was asserted here until 0.13.0 retired it. It said // a row's actions stay hidden until hover, which stopped being true when // makeover-webview 0.23.0 showed them at rest, and nothing had consumed // it for a release either way. assert_eq!(RowPart::Primary.intent(), "content"); assert_eq!(RowPart::Secondary.intent(), "content-secondary"); assert_eq!(RowPart::Meta.intent(), "content-muted"); } #[test] fn a_token_part_carries_no_intent_of_its_own() { // Each token carries its own tone, so a part-level intent underneath // would fight the thing sitting on it. Same reasoning as actions, which // is why they answer alike. assert_eq!(RowPart::Tokens.intent(), RowPart::Actions.intent()); assert_eq!(RowPart::Tokens.intent(), "content"); } #[test] fn the_two_temporal_kinds_are_the_two_that_name_a_moment() { // The pair is named once so a host with parsing to do asks here rather // than spelling it out, which is `offers_options`' reason. assert!(FieldKind::Date.temporal()); assert!(FieldKind::DateTime.temporal()); for kind in [ FieldKind::Text, FieldKind::Secret, FieldKind::Number, FieldKind::Email, FieldKind::Url, FieldKind::Tel, FieldKind::Range, FieldKind::Textarea, FieldKind::Rich, FieldKind::Select, FieldKind::Radio, FieldKind::Checkbox, FieldKind::File, FieldKind::Hidden, ] { assert!(!kind.temporal(), "{kind:?}"); } } #[test] fn a_date_carries_no_time_and_a_datetime_carries_no_zone() { // The formats are the whole reason the members are worth naming apart // from text, so the doc comments and the constants have to agree. A // host reading one and meeting the other is the silent failure. assert_eq!(DATE_FORMAT, "%Y-%m-%d"); assert!(!DATE_FORMAT.contains("%H"), "a day carries no hour"); assert_eq!(DATETIME_FORMAT, "%Y-%m-%dT%H:%M"); assert!( DATETIME_FORMAT.starts_with(DATE_FORMAT), "a moment starts with the day it is on" ); // Local, and that is a property of the value rather than an omission. assert!(!DATETIME_FORMAT.contains("%Z"), "no zone name"); assert!(!DATETIME_FORMAT.ends_with('Z'), "not UTC-stamped"); assert!(!DATETIME_FORMAT.contains("%S"), "no seconds by default"); } #[test] fn a_temporal_kind_takes_a_label_above_it_and_offers_no_options() { // Neither is a checkbox and neither is a fixed set, so both fall where // text does. Asserted because a new kind lands in three predicates and // only one of them is the interesting one. for kind in [FieldKind::Date, FieldKind::DateTime] { assert!(kind.visible(), "{kind:?}"); assert!(!kind.confidential(), "{kind:?}"); assert!(!kind.labels_itself(), "{kind:?}"); assert!(!kind.offers_options(), "{kind:?}"); } } #[test] fn a_cell_part_names_an_intent_and_only_the_value_is_text() { // The table half of what RowPart::intent does for rows. A cell holding // a control and a cell holding text answered alike until 0.14.0, and a // control in a cell took the cell's text colour. assert_eq!(CellPart::Value.intent(), "content"); for part in [CellPart::Tokens, CellPart::Actions, CellPart::Link] { // Each for its own reason -- a token carries its tone, an action is // a control, a link takes the action colour -- and all three reach // the intent inheriting already gives. assert_eq!(part.intent(), CellPart::Value.intent(), "{part:?}"); } } #[test] fn every_cell_part_answers_with_a_token_and_never_a_value() { for part in [ CellPart::Value, CellPart::Tokens, CellPart::Actions, CellPart::Link, ] { let intent = part.intent(); assert!(!intent.is_empty(), "{part:?} names nothing"); assert!(!intent.starts_with('#'), "{part:?} looks like a value"); } } #[test] fn a_separator_is_what_tells_a_section_from_a_subsection() { assert!(Heading::Section.separated()); assert!(!Heading::Subsection.separated()); assert!(!Heading::Page.separated()); } #[test] fn a_chosen_segment_is_held_in_and_a_chosen_tab_comes_forward() { assert_eq!(Selector::Segmented.chosen(), Depth::Well); assert_eq!(Selector::Toggle.chosen(), Depth::Well); // The exception, and the whole folder semantic: the open tab joins its // pane rather than sinking away from it. assert_eq!(Selector::Tabs.chosen(), Depth::Raised); // A held-in segment is indistinguishable from a pressed raised one, // which is the economy the light model buys over a colour swap. assert_eq!(Selector::Segmented.chosen(), Depth::Raised.pressed()); // A toggle stands alone; the other two are built out of parts that // touch. assert!(Selector::Segmented.abutting()); assert!(Selector::Tabs.abutting()); assert!(!Selector::Toggle.abutting()); } #[test] fn columns_are_peers_and_a_split_is_not() { // The distinction the member exists for. A split's two panes stand in a // master-detail relationship; columns choose nothing about each other. // Both are flat, so depth cannot tell them apart and the doc has to. assert_eq!(Region::Columns.depth(), Depth::Flat); assert_eq!(Region::Split.depth(), Depth::Flat); assert_ne!(Region::Columns, Region::Split); } #[test] fn columns_carry_no_count_and_no_share() { // The two things a board is always asked to carry and must not. How // many is what the children say; how wide is settled by "peers are // equal". // // The guard is the binding itself and it is a compile-time one: adding // a field to `Columns` stops this line compiling, which is a better // failure than any assertion about it. Written out rather than inlined // for exactly that reason. let columns: Region<'_> = Region::Columns; assert_eq!(columns.name(), None); } #[test] fn columns_are_described_and_the_escape_hatch_is_still_one_member() { // A board's contents are ordinary description all the way down, so a // renderer that does not lay them across still draws every column. // Stacking them vertically is honouring this member, not degrading it. assert!(Region::Columns.described()); assert!(!Region::Handover { name: "timeline" }.described()); } #[test] fn a_span_never_has_zero_minutes_however_it_is_asked_for() { // Every renderer divides by this. A caller passing a backwards or empty // span is a bug, but it is not a bug worth a panic three renderers deep. assert_eq!(Span::new(600, 600).length(), 1); assert_eq!(Span::new(600, 300).length(), 1); assert_eq!(Span::DAY.length(), 1440); } #[test] fn a_span_can_run_past_midnight_without_a_second_date() { // 22:00 to 02:00. The alternative was carrying a date, which drags a // timezone into the vocabulary for the sake of one night shift. let overnight = Span::new(1320, 1560); assert_eq!(overnight.length(), 240); assert!(overnight.holds(1500)); assert!(!overnight.holds(1200)); } #[test] fn overlap_is_computed_rather_than_declared() { // The reason Placement carries no `conflicts` flag: the times already // say it, and a second source for one fact is how a stale conflict // badge outlives the conflict. let morning = Placement::new(540, 60); // 09:00-10:00 let overlapping = Placement::new(570, 60); // 09:30-10:30 let after = Placement::new(600, 60); // 10:00-11:00 assert!(morning.overlaps(overlapping)); assert!(overlapping.overlaps(morning), "overlap is symmetric"); // Touching end to end is not overlapping: `to` is exclusive, so a // 10:00 start does not collide with a 10:00 end. assert!(!morning.overlaps(after)); assert!(!after.overlaps(morning)); } #[test] fn a_placement_is_always_drawable() { assert_eq!(Placement::new(540, 0).length(), 1); assert_eq!(Placement::new(540, 30).end(), 570); } #[test] fn a_track_places_the_fraction_every_renderer_would_otherwise_compute() { let day = Track::DAY; assert!((day.fraction(0) - 0.0).abs() < f32::EPSILON); assert!((day.fraction(720) - 0.5).abs() < f32::EPSILON); // Clamped rather than off the end: an event running past the span's // close draws at the edge, which beats panicking or drawing nowhere. assert!((day.fraction(2000) - 1.0).abs() < f32::EPSILON); } #[test] fn a_track_counts_its_slots_and_never_divides_by_zero() { assert_eq!(Track::DAY.slots(), 96); assert_eq!(Track::over(Span::new(540, 1020)).slots(), 32); // A span that does not divide evenly keeps a slot for its tail. assert_eq!(Track::over(Span::new(0, 50)).slots(), 4); // slot: 0 is a caller bug that reads as one slot, not a panic. let degenerate = Track { span: Span::DAY, slot: 0, tick: 60, unit: Unit::Minutes, }; assert_eq!(degenerate.slots(), 1); } #[test] fn a_track_carries_facts_and_no_presentation() { // The guard on the thing the withdrawn refusal was right about. If a // pixel measure, a scroll offset or a colour ever lands on Track, the // member has stopped being a fact about the data and the timeline // really has become a component library wearing a description's name. let day = Track::DAY; assert_eq!(day.span, Span::DAY); assert_eq!(day.slot, 15); assert_eq!(day.tick, 60); assert_eq!(day.unit, Unit::Minutes); // Three fields when this was written, and the fourth came here to say // why, which is the whole point of the assertion. `unit` is what the // integers COUNT -- a fact about the data, unavailable from the numbers // themselves, and the absence of it is what let a month strip render // under a wall clock. A fifth field still has to argue, and "the // renderer would find it handy" is still not the argument. // Destructured rather than rebuilt: this is the form that names every // field and stops compiling when a fifth arrives, without binding // anything a lint has to forgive. let Track { span: _, slot: _, tick: _, unit: _, } = day; } #[test] fn a_day_strip_is_the_same_arithmetic_under_a_different_unit() { // The probe that found the defect, kept as a test. Fifteen days from // day three, on a thirty-one day month: the geometry was always right // and only the label was wrong, which is why `unit` is a fact and not // presentation. let march = Track::days(Span::new(0, 31)); assert_eq!(march.slots(), 31); assert_eq!(march.unit, Unit::Days); let leave = Placement::new(2, 15); assert!((march.fraction(leave.at()) - 2.0 / 31.0).abs() < 0.0001); assert!((march.fraction(leave.end()) - 17.0 / 31.0).abs() < 0.0001); } #[test] fn a_pane_is_looked_into_and_a_band_is_not() { assert_eq!(Region::Pane.depth(), Depth::Well); assert_eq!(Region::Modal.depth(), Depth::Raised); for r in [ Region::Band, Region::Sidebar, Region::Group, Region::Split, Region::TabGroup, ] { assert_eq!(r.depth(), Depth::Flat, "{r:?} should carry no edge"); } } #[test] fn exactly_one_region_is_opaque() { // The escape hatch is one member and stays one member. If a second // undescribed region ever appears, the description has started // conceding rather than deferring. for r in [ Region::Band, Region::Sidebar, Region::Pane, Region::Group, Region::Split, Region::TabGroup, Region::Modal, // A widget is described, and that is the whole of what separates it // from a bespoke here. Both carry a name this crate never reads; // only one of them has contents under it that a renderer which does // not know the name can still walk. Region::Widget { name: "carousel" }, ] { assert!(r.described(), "{r:?} should be describable"); } assert!(!Region::Handover { name: "day-plan" }.described()); } #[test] fn a_region_nobody_converted_yet_is_not_a_region_the_app_gave_up_on() { // The whole of why this is two members. Both are opaque and neither is // described, so the old single member made them one value; a renderer // with no fill drew both as an empty box and said nothing either way. assert!(Region::Handover { name: "day-plan" }.owed()); assert!( !Region::Ceded { name: "revenue-chart" } .owed() ); // Everything that carries its own contents owes nothing, which is the // reading a renderer needs for the members it already draws. assert!(!Region::Pane.owed()); assert!(!Region::Widget { name: "carousel" }.owed()); // Opaqueness is the axis they still share. assert!(!Region::Ceded { name: "waveform" }.described()); assert_eq!(Region::Ceded { name: "waveform" }.name(), Some("waveform")); assert_eq!(Region::Ceded { name: "waveform" }.depth(), Depth::Flat); } #[test] fn a_group_contains_a_section_without_claiming_to_be_a_pane() { // The whole of why this is a member rather than a `Pane`. A pane is // looked into and scrolls; a group is neither, and four groups inside a // settings pane described as panes are four wells inside a well. assert_eq!(Region::Pane.depth(), Depth::Well); assert_eq!(Region::Group.depth(), Depth::Flat); assert_ne!(Region::Group, Region::Pane); // Described, and it carries no name: a group is a primitive every // renderer draws from scratch, which is what separates it from the two // members that do carry one. assert!(Region::Group.described()); assert_eq!(Region::Group.name(), None); } #[test] fn a_section_heading_names_a_block_that_now_exists() { // `Heading::Section` has said "names a block within the screen" since // 0.2.0 and there was no block. The pairing is the point, and it is the // reason a group carries no heading of its own: the heading is an // ordinary node in the body, and a group without one is legal. assert!(Heading::Section.separated()); assert_eq!(Region::Group.depth(), Depth::Flat); } #[test] fn a_widget_inherits_its_depth_the_way_a_bespoke_does() { // Stronger than the bespoke case: a widget is drawn by whichever // renderer recognises the name, so a depth chosen here would be this // crate deciding a carousel is raised on every host. assert_eq!(Region::Widget { name: "carousel" }.depth(), Depth::Flat); assert_eq!(Region::Widget { name: "pager" }.depth(), Depth::Flat); } #[test] fn a_name_is_readable_without_asking_which_member_carried_it() { // A renderer dispatching on a name wants the string, not the member. // Writing that `matches!` at each renderer is how the two drift apart. assert_eq!(Region::Widget { name: "carousel" }.name(), Some("carousel")); assert_eq!( Region::Handover { name: "day-plan" }.name(), Some("day-plan") ); for r in [ Region::Band, Region::Sidebar, Region::Pane, Region::Group, Region::Split, Region::TabGroup, Region::Modal, ] { assert_eq!(r.name(), None, "{r:?} names nothing an app chose"); } } #[test] fn a_bespoke_region_inherits_its_depth_rather_than_choosing_one() { // The app owns the contents, not the placement. An app that wants its // timeline in a well frames it in a Pane. assert_eq!(Region::Handover { name: "day-plan" }.depth(), Depth::Flat); assert_eq!(Region::Handover { name: "kanban" }.depth(), Depth::Flat); } #[test] fn a_screen_with_a_bespoke_region_is_still_a_whole_screen() { // The argument the member exists for: goingson's day-plan has to be // routable, or the description covers only the boring screens and the // interesting four need a second path beside the router. let day_plan = [ Region::Band, Region::Handover { name: "day-plan" }, Region::Sidebar, ]; assert_eq!(day_plan.iter().filter(|r| r.described()).count(), 2); assert_eq!(day_plan.iter().filter(|r| !r.described()).count(), 1); } #[test] fn a_secret_field_is_marked_as_one_and_a_hidden_field_is_not_drawn() { let secret = Field::new(FieldKind::Secret, "password", "Password"); assert!(secret.kind.confidential()); assert!(secret.kind.visible()); assert!(!FieldKind::Hidden.visible()); // Nothing else is confidential, or the marker means nothing. for k in [ FieldKind::Text, FieldKind::Number, FieldKind::Textarea, FieldKind::Rich, FieldKind::Select, FieldKind::Checkbox, FieldKind::Hidden, ] { assert!(!k.confidential(), "{k:?} should not be confidential"); } // Only a checkbox carries its own label. assert!(FieldKind::Checkbox.labels_itself()); assert!(!FieldKind::Text.labels_itself()); } #[test] fn a_plain_field_offers_nothing_and_a_select_offers_its_options() { let text = Field::new(FieldKind::Text, "title", "Title"); assert!(text.options.is_empty()); assert_eq!(text.placeholder, None); let sizes = [Choice::plain("small"), Choice::plain("large")]; let select = Field::select("size", "Size", &sizes); assert_eq!(select.kind, FieldKind::Select); assert_eq!(select.options.len(), 2); } #[test] fn a_choice_says_what_submits_and_what_is_read_apart() { // The whole reason it is two strings. `plain` is the case where they // coincide, and it is a shorthand rather than the general shape. let plain = Choice::plain("7"); assert_eq!((plain.value, plain.label), ("7", "7")); let spelled = Choice::new("7", "One week"); assert_ne!(spelled.value, spelled.label); assert!( spelled.available(), "an option is pickable until it says not" ); } #[test] fn a_candidate_carries_the_line_that_tells_it_from_its_neighbours() { // The gap this type was born for: two candidates whose labels read // alike, told apart by the second string and by nothing else. The // measured site is the MNW tag box, where "Format" is a leaf under // audio, software, writing and video. let audio = Candidate::new("audio/format", "Format").detailed("Audio"); let writing = Candidate::new("writing/format", "Format").detailed("Writing"); assert_eq!(audio.label, writing.label); assert_ne!(audio.detail, writing.detail); assert_ne!( audio, writing, "two rows a user cannot tell apart are two rows the type can" ); } #[test] fn a_candidate_is_read_differently_from_an_option_and_written_the_same() { // The ruling's own distinction, held as a test so the two types do not // drift back together. Submitting is identical; the second line is the // whole of what differs, and it is absent by default because a list of // distinct labels wants nothing there. let candidate = Candidate::plain("rust"); assert_eq!((candidate.value, candidate.label), ("rust", "rust")); assert_eq!( candidate.detail, None, "one line unless the route says otherwise" ); let option = Choice::plain("rust"); assert_eq!( (candidate.value, candidate.label), (option.value, option.label) ); } #[test] fn a_radio_asks_the_same_question_as_a_select_and_is_not_the_same_kind() { // Both offer a fixed set and both read `options`, so the two // constructors differ in exactly one thing. That one thing is the // point: a renderer decides whether the alternatives are readable // without opening anything, and it can only decide that if the // description said which question was asked. let styles = [ Choice::new("copy", "Copy samples in"), Choice::new("reference", "Reference in place"), ]; let radio = Field::radio("storage", "Storage style", &styles); let select = Field::select("storage", "Storage style", &styles); assert_eq!(radio.kind, FieldKind::Radio); assert_ne!(radio.kind, select.kind); assert_eq!(radio.options, select.options); assert_eq!( Field { kind: select.kind, ..radio }, select ); } #[test] fn an_unavailable_option_cannot_be_silent_about_it() { // The whole content of the one-member shape: saying an option is not // pickable and saying why are the same act, so the greyed-out-with-no- // reason state is unsayable rather than merely discouraged. let multi = Choice::new("multi", "Multi-sample").unless("Drop a second sample onto the keyboard."); assert!(!multi.available()); assert_eq!( multi.unavailable, Some("Drop a second sample onto the keyboard.") ); // And the option is still in the list, carrying what it submits, so a // renderer draws it rather than the app dropping it. assert_eq!(multi.value, "multi"); assert_eq!(multi.label, "Multi-sample"); } #[test] fn an_option_can_say_what_picking_it_means_and_why_it_cannot_be_picked() { // `5e21dcfc`. Two different sentences about one option, and an option // that has both has said two things: what the tier is, and that it is // not available yet. Folding them would be the label-folding this // member exists to end. let tier = Choice::new("24", "Small Files") .detailing("$24/mo. 2GB/file, 100GB total. Fits audio, plugins, binaries.") .unless("Sold out while the founder window is open."); assert_eq!( tier.detail, Some("$24/mo. 2GB/file, 100GB total. Fits audio, plugins, binaries.") ); assert_eq!( tier.unavailable, Some("Sold out while the founder window is open.") ); assert!(!tier.available()); // Neither is implied by the other, which is what keeps a renderer from // reading a detail as a reason: an ordinary option with a second line // is still pickable. let plain = Choice::new("free", "Free").detailing("No charge. Available to everyone."); assert!(plain.available()); assert_eq!(plain.detail, Some("No charge. Available to everyone.")); assert_eq!(Choice::new("free", "Free").detail, None); } #[test] fn a_range_carries_both_ends_and_a_validated_number_need_not() { // The distinction the kind exists for, asserted rather than only // written down: bounds are a rule for one and the control itself for // the other. let threshold = Field::range("review", "Review above", "0", "1"); assert_eq!(threshold.kind, FieldKind::Range); assert!(threshold.bounded()); assert_eq!(threshold.min, Some("0")); assert_eq!(threshold.max, Some("1")); // Granularity is the host's until an app says otherwise. assert_eq!(threshold.step, None); // goingson's duration: a typed number with a floor, and it must not // read as a slider. let minutes = Field { min: Some("1"), ..Field::new(FieldKind::Number, "minutes", "Minutes") }; assert_ne!(minutes.kind, FieldKind::Range); assert!(!minutes.bounded(), "one end is a rule, not an extent"); } #[test] fn a_range_described_with_one_end_says_so_rather_than_being_refused() { // Nothing here enforces the pair, for the reason nothing here enforces // `required`: the description states the constraint and the renderer // asks. What it must not do is look bounded. let half = Field { max: Some("1"), ..Field::new(FieldKind::Range, "review", "Review above") }; assert!(!half.bounded()); } #[test] fn exactly_the_option_taking_kinds_say_so() { // The renderers branch on this rather than on a list of their own, so // a kind added without a decision here renders its options nowhere. assert!(FieldKind::Select.offers_options()); assert!(FieldKind::Radio.offers_options()); for kind in [ FieldKind::Text, FieldKind::Secret, FieldKind::Number, FieldKind::Email, FieldKind::Url, FieldKind::Tel, FieldKind::Range, FieldKind::Textarea, FieldKind::Rich, FieldKind::Checkbox, FieldKind::Hidden, ] { assert!(!kind.offers_options(), "{kind:?} does not offer options"); } } #[test] fn a_radio_group_takes_a_label_even_though_its_options_carry_their_own() { // The near-miss: each option is labelled beside its own button, so a // renderer could plausibly read the group as self-labelling and drop // the question. Checkbox is the only kind that does that. assert!(!FieldKind::Radio.labels_itself()); assert!(FieldKind::Checkbox.labels_itself()); } #[test] fn a_select_with_no_options_is_sayable() { // An app whose option list has not loaded has exactly this. Making it // unrepresentable would push the state somewhere less visible, and a // renderer drawing an empty select reports it on screen. let loading = Field::select("project", "Project", &[]); assert!(loading.options.is_empty()); } #[test] fn the_description_carries_the_question_and_never_the_answer() { // The line 0.8.0 drew. Placeholder and options are properties of what // is being asked; the current value is what came back, and no field // here holds one. let f = Field { placeholder: Some("yyyy-mm-dd"), ..Field::new(FieldKind::Text, "due", "Due") }; assert_eq!(f.placeholder, Some("yyyy-mm-dd")); // A placeholder is not a label, and having one does not excuse the // field from carrying the other. assert_eq!(f.label, "Due"); } #[test] fn a_field_reports_its_own_error_state() { let mut f = Field::new(FieldKind::Text, "title", "Title"); assert!(!f.invalid()); f.error = Some("Required"); assert!(f.invalid()); } #[test] fn columns_drop_by_priority_and_never_by_position() { let cols = [ Column { width: Width::Fill, priority: Priority::Essential, ..Column::new("Title") }, Column { width: Width::Fixed, priority: Priority::Secondary, ..Column::new("Due") }, Column { width: Width::Fixed, priority: Priority::Optional, ..Column::new("Estimate") }, ]; // Widest: everything survives. assert_eq!( cols.iter() .filter(|c| c.kept_at(Priority::Optional)) .count(), 3 ); // Narrower: the optional column goes first. let kept: Vec<_> = cols .iter() .filter(|c| c.kept_at(Priority::Secondary)) .map(|c| c.name) .collect(); assert_eq!(kept, ["Title", "Due"]); // Narrowest: only what identifies the row. let kept: Vec<_> = cols .iter() .filter(|c| c.kept_at(Priority::Essential)) .map(|c| c.name) .collect(); assert_eq!(kept, ["Title"]); } #[test] fn inserting_a_column_does_not_move_what_gets_dropped() { // The bug the ordinal form has and this form cannot: goingson hides // `nth-child(n+5)` against a seven-column table, so a column inserted // anywhere to the left silently hides a different one. let before = [ Column::new("Title"), Column { width: Width::Fixed, priority: Priority::Optional, ..Column::new("Estimate") }, ]; let after = [ Column::new("Title"), Column::new("Project"), // inserted Column { width: Width::Fixed, priority: Priority::Optional, ..Column::new("Estimate") }, ]; fn dropped<'a>(cols: &[Column<'a>]) -> Vec<&'a str> { cols.iter() .filter(|c| !c.kept_at(Priority::Secondary)) .map(|c| c.name) .collect() } assert_eq!(dropped(&before), ["Estimate"]); assert_eq!(dropped(&after), ["Estimate"]); } #[test] fn an_arrangement_carries_the_tab_group_as_a_modifier() { // goingson uses the tab group inside the content region rather than // instead of one, so it is not a third arrangement. let go = Arrangement::list_detail(true); let plain = Arrangement::list_detail(false); assert_ne!(go, plain); assert_ne!(go, Arrangement::sidebar_content()); } #[test] fn a_share_is_a_proportion_and_resolves_the_same_way_everywhere() { // The point of the member: a terminal reading columns and a webview // reading a grid honour one fact, so two hosts showing one screen agree // about its proportions. assert_eq!(Share::LIST.as_percent(), 40); assert_eq!(Share::LIST.of(100), 40); assert_eq!( Share::SIDEBAR.of(96), 24, "quasi-tui's 24 columns, said as a quarter" ); } #[test] fn a_region_never_resolves_to_nothing() { // A region the description named should be visible. A zero-width one // reads on screen as a region that vanished, which is the hardest kind // of bug to find from what is drawn. assert_eq!(Share::percent(5).of(1), 1); assert_eq!(Share::percent(5).of(0), 1); } #[test] fn a_share_outside_the_range_is_clamped_rather_than_refused() { assert_eq!(Share::percent(0), Share::percent(5)); assert_eq!(Share::percent(200), Share::percent(95)); } #[test] fn the_share_rides_on_the_arrangement_that_knows_which_question_it_is() { // How much a sidebar takes and how much a list side takes are different // questions, and this enum is the only thing that knows which is being // asked. assert_eq!(Arrangement::sidebar_content().share(), Some(Share::SIDEBAR)); assert_eq!(Arrangement::list_detail(false).share(), Some(Share::LIST)); // One region divides nothing, so there is no share to answer with. assert_eq!(Arrangement::Single.share(), None); assert_eq!( Arrangement::Single.with_share(Share::percent(20)), Arrangement::Single ); let narrow = Arrangement::sidebar_content().with_share(Share::percent(20)); assert_eq!(narrow.share(), Some(Share::percent(20))); assert!(matches!(narrow, Arrangement::SidebarContent { .. })); } #[test] fn a_measure_defaults_to_the_one_53_of_69_templates_asked_for() { // The default is meaningful: a screen nobody said anything about uses // the window it was given. assert_eq!(Measure::default(), Measure::Wide); assert_eq!(Measure::Reading.as_str(), "reading"); } #[test] fn readiness_names_the_state_and_not_the_shimmer() { // Two members and no third. If a skeleton ever appears in this enum, // the deferral rule has been broken. assert_ne!(Readiness::Ready, Readiness::Pending); } #[test] fn a_window_with_no_length_still_answers_what_it_can() { // The uncounted case is the common one, not the degenerate one: a query // that asked for 51 to learn there were more than 50 knows there are, // and not how many. let uncounted = Window::new(100, 50); assert_eq!(uncounted.index(), Some(2)); assert_eq!(uncounted.windows(), None); assert!(uncounted.has_before()); // Unknown length cannot rule out more, and offering a way forward that // turns out empty is the cheaper mistake. assert!(uncounted.has_after()); } #[test] fn a_counted_window_knows_where_it_ends() { let last = Window::new(350, 50).of(400); assert_eq!(last.index(), Some(7)); assert_eq!(last.windows(), Some(8)); assert!(last.has_before()); assert!(!last.has_after()); let first = Window::new(0, 50).of(400); assert!(!first.has_before()); assert!(first.has_after()); } #[test] fn a_window_that_does_not_divide_evenly_rounds_up() { // 401 rows in pages of 50 is eight pages and a straggler, which is nine // pages. Rounding down would make the last one unreachable. assert_eq!(Window::new(0, 50).of(401).windows(), Some(9)); } #[test] fn a_zero_count_answers_none_rather_than_dividing() { let empty = Window::new(0, 0).of(400); assert_eq!(empty.index(), None); assert_eq!(empty.windows(), None); // And it still clamps rather than panicking. assert_eq!(Window::new(900, 0).of(400).clamped().from, 399); } #[test] fn a_window_past_the_end_clamps_inside_rather_than_vanishing() { // `Slot::current`'s reasoning, one layer down: a description pointing // past the end is a host bug, and answering it by drawing nothing // reports a region that vanished. assert_eq!(Window::new(900, 50).of(400).clamped().from, 350); // Nothing to clamp against when the length is unknown. assert_eq!(Window::new(900, 50).clamped().from, 900); } #[test] fn a_carousel_frame_is_a_window_of_one() { // The shape a carousel instantiates. Same code as a paged list, which is // the whole reason `Window` exists rather than two copies of it. let third = Window::frame(2, 5); assert_eq!(third.index(), Some(2)); assert_eq!(third.windows(), Some(5)); assert!(third.has_before()); assert!(third.has_after()); let last = Window::frame(4, 5); assert!(!last.has_after()); } #[test] fn numbered_pages_read_from_one_and_load_more_has_no_page() { // The page number is read aloud, so it is one-based; `Window::index` is // the zero-based form for indexing. let third = Paging::pages(100, 50).of(400); assert_eq!(third.page(), Some(3)); assert_eq!(third.pages_total(), Some(8)); assert_eq!(third.total(), Some(400)); assert!(third.has_previous()); assert!(third.has_more()); // Load-more grew a window from the start, so "page 2" would name // nothing and the type says so rather than inventing one. let grown = Paging::more(150).of(400); assert_eq!(grown.page(), None); assert_eq!(grown.pages_total(), None); assert_eq!(grown.shown(), 150); assert!(!grown.has_previous()); assert!(grown.has_more()); } #[test] fn an_uncounted_paging_offers_forward_and_admits_no_total() { // What a host that will not pay for a COUNT describes. `None` here is // permanent: a total arriving later would widen the text that prints it, // which is the reflow "first paint is final paint" forbids. let feed = Paging::more(50); assert_eq!(feed.total(), None); assert_eq!(feed.pages_total(), None); assert_eq!(feed.remaining(), None); assert!(feed.has_more()); } #[test] fn what_is_left_is_derived_and_never_underflows() { assert_eq!(Paging::more(150).of(400).remaining(), Some(250)); assert_eq!(Paging::pages(350, 50).of(400).remaining(), Some(0)); // A host that overshot its own total gets zero rather than a wrapped // usize, which would print as "18446744073709551516 remaining". assert_eq!(Paging::more(500).of(400).remaining(), Some(0)); } #[test] fn a_fallback_is_authored_and_a_group_cannot_omit_it() { // No `Default`. The compiler is what enforces rule 2, so the assertion // that matters is one this file cannot write; what it can say is that // the four authored answers are distinct and none is privileged. let all = [ Fallback::Wrap, Fallback::Stack, Fallback::Shed, Fallback::Menu, ]; for (i, a) in all.iter().enumerate() { for b in &all[i + 1..] { assert_ne!(a, b); } } } #[test] fn shedding_stops_at_essential_whatever_the_group_holds() { // Priority is read the same way for a group member as for a column, // which is the whole claim of generalising it off `Column`. let members = [ ("tabs", Priority::Essential), ("search", Priority::Secondary), ("count", Priority::Optional), ]; let kept: Vec<_> = members .iter() .filter(|(_, p)| *p >= Priority::Essential) .map(|(n, _)| *n) .collect(); assert_eq!(kept, ["tabs"]); } #[test] fn a_role_says_what_a_part_is_worth_when_the_run_does_not_fit() { // The row still identifies itself after everything droppable has gone, // which is the property the ladder exists for. assert_eq!(RowPart::Primary.priority(), Priority::Essential); // A control is not a fact. Room comes out of what the row says, never // out of what it offers. assert_eq!(RowPart::Actions.priority(), Priority::Essential); assert_eq!(RowPart::Meta.priority(), Priority::Optional); assert_eq!(RowPart::Proportion.priority(), Priority::Optional); assert_eq!(RowPart::Secondary.priority(), Priority::Secondary); // Tokens sit in the middle deliberately: a toned badge is often the // most scannable thing in a row, so it does not go first. assert_eq!(RowPart::Tokens.priority(), Priority::Secondary); } #[test] fn a_run_is_one_line_unless_the_description_says_two() { // The default is what every part did before flows existed, so a // description written against the old vocabulary keeps its rendering. assert_eq!(Flow::default(), Flow::Tight); assert_eq!(Flow::Tight.lines(), 1); assert_eq!(Flow::Relaxed.lines(), 2); } #[test] fn an_unknown_flow_reads_as_one_line() { // `#[non_exhaustive]`'s cost, taken deliberately. A tier added upstream // reaches an old renderer as one line rather than as a build break, and // one line is the reading that cannot break a neighbour's layout. The // match in `lines` is what this holds; it fails if a new tier is given // an arm that returns something unbounded. for flow in [Flow::Tight, Flow::Relaxed] { assert!((1..=2).contains(&flow.lines())); } } #[test] fn an_awaiting_mark_is_indeterminate_until_something_is_measured() { // The default is the common case: a call waits, and nothing about it is // countable. A determinate bar is the exception and says so. assert_eq!(Awaiting::default(), Awaiting::unmeasured()); assert!(!Awaiting::unmeasured().is_determinate()); assert!(Awaiting::of(40 * 1024 * 1024).is_determinate()); assert_eq!(Awaiting::of(7).amount, Some(7)); } // A slider is a fraction and a mapping // // `Curve` is the one thing in this crate that computes rather than // describes, and it does so because four renderers would otherwise each // write these two formulas and drift. So the formulas are pinned here. /// The bounds of audiofiles' envelope attack, the curve's first consumer. const ATTACK: (f64, f64) = (0.001, 5.0); #[test] fn a_curve_is_linear_with_no_step_until_a_field_says_otherwise() { assert_eq!(Curve::default(), Curve::Linear { step: None }); let plain = Field::range("t", "T", "0", "1"); assert_eq!(plain.curve, Curve::Linear { step: None }); assert_eq!(plain.curve.step(), None); } #[test] fn every_curve_carries_its_own_granularity() { assert_eq!(Curve::Linear { step: Some("0.01") }.step(), Some("0.01")); assert_eq!( Curve::Logarithmic { step: Some("0.001") } .step(), Some("0.001") ); } #[test] fn both_ends_of_the_track_are_the_bounds_under_either_curve() { // `min` and `max` are `f(0)` and `f(1)`. That is the whole reframe, and // it has to hold for a mapping that is not the identity or the bounds // have stopped meaning what the field says they mean. let (min, max) = ATTACK; for curve in [ Curve::Linear { step: None }, Curve::Logarithmic { step: None }, ] { assert!((curve.value_at(0.0, min, max) - min).abs() < 1e-12); assert!((curve.value_at(1.0, min, max) - max).abs() < 1e-12); } } #[test] fn a_linear_midpoint_is_the_average_and_a_ratio_midpoint_is_the_geometric_mean() { let (min, max) = ATTACK; let linear = Curve::Linear { step: None }.value_at(0.5, min, max); assert!((linear - 2.5005).abs() < 1e-9); // The reason the envelope is not linear: half way along a log track is // 70 ms, and half way along a linear one is 2.5 seconds. Every attack a // sampler is actually played with lives below the first. let ratio = Curve::Logarithmic { step: None }.value_at(0.5, min, max); assert!((ratio - (min * max).sqrt()).abs() < 1e-12); assert!(ratio < 0.08); } #[test] fn a_position_and_a_value_round_trip_under_either_curve() { let (min, max) = ATTACK; for curve in [ Curve::Linear { step: None }, Curve::Logarithmic { step: None }, ] { for position in [0.0, 0.1, 0.25, 0.5, 0.75, 0.99, 1.0] { let back = curve.position_of(curve.value_at(position, min, max), min, max); assert!( (back - position).abs() < 1e-9, "{curve:?} lost {position} (got {back})" ); } } } #[test] fn a_ratio_curve_across_zero_is_drawn_linearly_rather_than_refused() { // An envelope's sustain is a 0-to-1 level. A constant ratio is // undefined there, and the answer is the linear mapping rather than a // NaN reaching a renderer that would paint it. let curve = Curve::Logarithmic { step: None }; assert!(!curve.is_ratio(0.0, 1.0)); assert!((curve.value_at(0.5, 0.0, 1.0) - 0.5).abs() < 1e-12); assert!(curve.value_at(0.5, -96.0, -20.0).is_finite()); assert!(curve.is_ratio(ATTACK.0, ATTACK.1)); } #[test] fn a_track_with_no_extent_has_one_value_on_it() { for curve in [ Curve::Linear { step: None }, Curve::Logarithmic { step: None }, ] { assert!((curve.value_at(0.7, 4.0, 4.0) - 4.0).abs() < f64::EPSILON); assert!(curve.position_of(4.0, 4.0, 4.0).abs() < f64::EPSILON); // Inverted bounds are the same degenerate answer, not a negative // extent a renderer would draw backwards. assert!((curve.value_at(0.7, 9.0, 2.0) - 9.0).abs() < f64::EPSILON); } } #[test] fn a_position_or_a_value_outside_the_track_is_clamped_to_it() { let (min, max) = ATTACK; let curve = Curve::Logarithmic { step: None }; assert!((curve.value_at(-3.0, min, max) - min).abs() < 1e-12); assert!((curve.value_at(4.0, min, max) - max).abs() < 1e-12); assert!(curve.position_of(0.0, min, max).abs() < 1e-12); assert!((curve.position_of(500.0, min, max) - 1.0).abs() < 1e-12); } #[test] fn a_typed_number_keeps_its_own_step_and_a_range_reads_its_curve() { // The split the 0.32.0 narrowing is: two granularities that were one // member, and the kinds that take them do not overlap. let typed = Field { step: Some("5"), ..Field::new(FieldKind::Number, "port", "Port") }; assert_eq!(typed.step, Some("5")); let slid = Field { curve: Curve::Logarithmic { step: Some("0.001"), }, ..Field::range("attack", "Attack", "0.001", "5") }; assert_eq!(slid.step, None); assert_eq!(slid.curve.step(), Some("0.001")); } #[test] fn a_theme_picker_offers_themes_and_no_options() { // The substitution hazard the constructor exists against: `options` is // right there and reads as if it would work, and a renderer walking it // for a theme picker draws an empty control. let themes = [ ThemeChoice::new("goingson", "GoingsOn", ThemeVariant::Light, Contrast::High), ThemeChoice::new("dracula", "Dracula", ThemeVariant::Dark, Contrast::Standard), ]; let field = Field::theme("theme", "Theme", &themes); assert_eq!(field.kind, FieldKind::Theme); assert!(field.kind.offers_themes()); assert!(!field.kind.offers_options()); assert_eq!(field.themes.len(), 2); assert!(field.options.is_empty()); assert_eq!(field.follows, None); } #[test] fn following_carries_the_store_s_own_spelling() { // Not hardcoded here: the value belongs to the app's config table, and // this crate holds no facts about somebody else's store. let field = Field::theme("theme", "Theme", &[]).following(Choice::new("system", "Follow System")); let follow = field.follows.expect("the row was offered"); assert_eq!(follow.value, "system"); assert_eq!(follow.label, "Follow System"); } #[test] fn a_picker_with_nothing_resolved_is_sayable() { // A machine whose theme directories hold nothing. The description is // true and a renderer says so on screen rather than in a log, which is // `Field::options`' own arrangement. let field = Field::theme("theme", "Theme", &[]); assert!(field.themes.is_empty()); } #[test] fn every_kind_but_theme_offers_no_themes() { for kind in [ FieldKind::Text, FieldKind::Select, FieldKind::Radio, FieldKind::Checkbox, FieldKind::File, FieldKind::Hidden, ] { assert!( !kind.offers_themes(), "{kind:?} does not read Field::themes" ); } } #[test] fn a_contrast_tier_reads_worst_first() { // Matches `makeover::ContrastTier`, so an adopter's conversion cannot // invert an ordering by accident and a sort agrees across the seam. assert!(Contrast::Low < Contrast::Standard); assert!(Contrast::Standard < Contrast::High); } #[test] fn the_groups_and_badges_have_one_spelling_each() { // The whole argument for these living here: three renderers picking // their own is one picker reading three ways. assert_eq!(ThemeVariant::Light.heading(), "Light"); assert_eq!(ThemeVariant::Dark.heading(), "Dark"); assert_eq!(ThemeVariant::HighContrast.heading(), "High Contrast"); assert_eq!(ThemeVariant::HighContrast.as_str(), "high-contrast"); assert_eq!(Contrast::High.badge(), "AA"); assert_eq!(Contrast::Standard.badge(), "OK"); assert_eq!(Contrast::Low.badge(), "low"); } #[test] fn the_variant_spelling_matches_the_theme_file_s_own() { // The seam this enum is duplicated across. `makeover::parse_meta` reads // `meta.variant` as one of these three strings; a rename on either side // that does not move together silently regroups every picker. for (variant, spelling) in [ (ThemeVariant::Light, "light"), (ThemeVariant::Dark, "dark"), (ThemeVariant::HighContrast, "high-contrast"), ] { assert_eq!(variant.as_str(), spelling); assert_eq!(variant.to_string(), spelling); } }