use super::{Change, Intent as _, Syntax};
#[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,
};
}
#[test]
fn an_unclassified_run_is_plain_and_plain_is_the_default() {
assert_eq!(Syntax::default(), Syntax::Plain);
assert_eq!(Syntax::Plain.name(), "plain");
}
#[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());
}
#[test]
fn an_unmarked_diff_line_is_context() {
assert_eq!(Change::default(), Change::Context);
assert_eq!(Change::Context.token(), "content");
}
#[test]
fn the_two_sides_of_a_change_take_the_status_intents() {
assert_eq!(Change::Added.token(), "success");
assert_eq!(Change::Removed.token(), "danger");
}
#[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());
assert!(Nesting::top() < under);
}
use super::*;
#[test]
fn a_markdown_field_is_multiline_and_offers_nothing() {
assert!(FieldKind::Rich.multiline());
assert!(FieldKind::Textarea.multiline());
assert!(!FieldKind::Text.multiline());
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());
assert!(FieldKind::Interval.measurable());
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() {
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() {
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() {
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() {
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"));
assert!(axis.error.is_none());
}
#[test]
fn only_a_subtree_prunes_and_only_the_listing_modes_offer_values() {
assert!(Selecting::Subtree.prunes());
for mode in [
Selecting::OneOf,
Selecting::AnyOf,
Selecting::Range,
Selecting::Text,
] {
assert!(!mode.prunes(), "{mode:?}");
}
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() {
assert!(Standing::Inherited.in_force());
assert!(!Standing::Inherited.is_picked());
assert!(Standing::Taken.in_force());
assert!(Standing::Taken.is_picked());
assert!(Standing::Pruned.is_picked());
assert!(!Standing::Pruned.in_force());
assert!(!Standing::Open.is_picked());
assert!(!Standing::Open.in_force());
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());
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),
];
assert!(!Facet::new("Tag", Selecting::Subtree, &untouched).engaged());
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() {
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());
}
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() {
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)
);
assert_eq!(Accepted::Type("text/csv").family(), None);
assert_eq!(Accepted::Suffix(".mp3").family(), None);
assert_eq!(Accepted::Suffix(".tar.gz").family(), None);
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() {
const MEDIA: &[Accepted<'_>] = &[
Accepted::Family(Family::Image),
Accepted::Family(Family::Video),
];
assert!(Field::upload("media", "Media", MEDIA).accepts_media());
const BUILDS: &[Accepted<'_>] = &[Accepted::Suffix(".zip"), Accepted::Suffix(".dmg")];
assert!(!Field::upload("build", "Build", BUILDS).accepts_media());
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() {
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() {
assert_eq!(Showing::default(), Showing::All);
assert!(!Showing::All.selective());
}
#[test]
fn only_a_disclosure_can_show_nothing() {
assert!(Showing::AtMostOne.dismissible());
assert!(!Showing::One.dismissible());
assert!(!Showing::All.dismissible());
assert!(Showing::One.selective());
assert!(Showing::AtMostOne.selective());
}
#[test]
fn an_empty_region_is_not_a_broken_one() {
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() {
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() {
assert_eq!(Sort::Ascending.glyph(), "\u{25B2}");
assert_eq!(Sort::Descending.glyph(), "\u{25BC}");
assert_ne!(Sort::Ascending.glyph(), Sort::Descending.glyph());
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() {
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() {
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);
assert_eq!(Figure::new("3.1%", "Conversion").change, None);
}
#[test]
fn a_figures_value_is_text_because_only_the_app_knows_what_it_is() {
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() {
assert_eq!(RowPart::Proportion.intent(), RowPart::Tokens.intent());
}
#[test]
fn a_file_field_is_drawn_and_offers_no_options() {
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() {
let field = Field {
max_length: Some(100),
min: Some("1"),
max: Some("240"),
required: true,
..Field::new(FieldKind::Number, "minutes", "Minutes")
};
assert!(!field.invalid());
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() {
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() {
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() {
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() {
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);
assert_eq!(Meter::new(9, 10).tone, Tone::Neutral);
}
#[test]
fn an_act_is_reachable_until_it_is_disabled() {
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() {
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() {
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() {
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() {
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() {
assert!(State::Disabled.suppresses_interaction());
}
#[test]
fn disabled_resolves_against_an_intent_makeover_already_derives() {
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() {
assert_eq!(Depth::Sunken.fill(), Some(Fill::Sunken));
assert_eq!(Depth::Sunken.bevel(), None);
}
#[test]
fn sunken_and_flat_are_different_claims() {
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() {
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() {
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() {
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);
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)
);
assert_eq!(Depth::Flat.pressed(), Depth::Flat);
assert_eq!(Depth::Well.pressed(), Depth::Well);
assert_eq!(Depth::Overlay.pressed(), Depth::Overlay);
}
#[test]
fn an_overlay_is_lifted_rather_than_edged() {
assert_eq!(Depth::Overlay.fill(), Some(Fill::Overlay));
assert_eq!(Depth::Overlay.bevel(), None);
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() {
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);
assert!(Field::new(FieldKind::Text, "title", "Title").note.is_none());
}
#[test]
fn neutral_is_content_not_muted_content() {
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");
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() {
assert!(!Token::Badge.interactive());
assert!(Token::Chip { removable: false }.interactive());
assert!(Token::Chip { removable: true }.interactive());
assert_eq!(Token::Badge.depth(false), Depth::Flat);
assert_eq!(Token::Badge.depth(true), Depth::Flat);
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());
assert_eq!(Notice::Toast.fill(), Fill::Overlay);
assert_eq!(Notice::Banner.fill(), Fill::Raised);
}
#[test]
fn emphasis_falls_off_down_the_row() {
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() {
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() {
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() {
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"
);
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() {
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() {
assert_eq!(CellPart::Value.intent(), "content");
for part in [CellPart::Tokens, CellPart::Actions, CellPart::Link] {
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);
assert_eq!(Selector::Tabs.chosen(), Depth::Raised);
assert_eq!(Selector::Segmented.chosen(), Depth::Raised.pressed());
assert!(Selector::Segmented.abutting());
assert!(Selector::Tabs.abutting());
assert!(!Selector::Toggle.abutting());
}
#[test]
fn columns_are_peers_and_a_split_is_not() {
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() {
let columns: Region<'_> = Region::Columns;
assert_eq!(columns.name(), None);
}
#[test]
fn columns_are_described_and_the_escape_hatch_is_still_one_member() {
assert!(Region::Columns.described());
assert!(!Region::Handover { name: "timeline" }.described());
}
#[test]
fn a_span_never_has_zero_minutes_however_it_is_asked_for() {
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() {
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() {
let morning = Placement::new(540, 60); let overlapping = Placement::new(570, 60); let after = Placement::new(600, 60);
assert!(morning.overlaps(overlapping));
assert!(overlapping.overlaps(morning), "overlap is symmetric");
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);
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);
assert_eq!(Track::over(Span::new(0, 50)).slots(), 4);
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() {
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);
let Track {
span: _,
slot: _,
tick: _,
unit: _,
} = day;
}
#[test]
fn a_day_strip_is_the_same_arithmetic_under_a_different_unit() {
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() {
for r in [
Region::Band,
Region::Sidebar,
Region::Pane,
Region::Group,
Region::Split,
Region::TabGroup,
Region::Modal,
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() {
assert!(Region::Handover { name: "day-plan" }.owed());
assert!(
!Region::Ceded {
name: "revenue-chart"
}
.owed()
);
assert!(!Region::Pane.owed());
assert!(!Region::Widget { name: "carousel" }.owed());
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() {
assert_eq!(Region::Pane.depth(), Depth::Well);
assert_eq!(Region::Group.depth(), Depth::Flat);
assert_ne!(Region::Group, Region::Pane);
assert!(Region::Group.described());
assert_eq!(Region::Group.name(), None);
}
#[test]
fn a_section_heading_names_a_block_that_now_exists() {
assert!(Heading::Section.separated());
assert_eq!(Region::Group.depth(), Depth::Flat);
}
#[test]
fn a_widget_inherits_its_depth_the_way_a_bespoke_does() {
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() {
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() {
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() {
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());
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");
}
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() {
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() {
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() {
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() {
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() {
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.")
);
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() {
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());
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() {
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"));
assert_eq!(threshold.step, None);
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() {
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() {
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() {
assert!(!FieldKind::Radio.labels_itself());
assert!(FieldKind::Checkbox.labels_itself());
}
#[test]
fn a_select_with_no_options_is_sayable() {
let loading = Field::select("project", "Project", &[]);
assert!(loading.options.is_empty());
}
#[test]
fn the_description_carries_the_question_and_never_the_answer() {
let f = Field {
placeholder: Some("yyyy-mm-dd"),
..Field::new(FieldKind::Text, "due", "Due")
};
assert_eq!(f.placeholder, Some("yyyy-mm-dd"));
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,
kind: ColumnKind::Text,
..Column::new("Title")
},
Column {
width: Width::Fixed,
priority: Priority::Secondary,
kind: ColumnKind::Text,
..Column::new("Due")
},
Column {
width: Width::Fixed,
priority: Priority::Optional,
kind: ColumnKind::Text,
..Column::new("Estimate")
},
];
assert_eq!(
cols.iter()
.filter(|c| c.kept_at(Priority::Optional))
.count(),
3
);
let kept: Vec<_> = cols
.iter()
.filter(|c| c.kept_at(Priority::Secondary))
.map(|c| c.name)
.collect();
assert_eq!(kept, ["Title", "Due"]);
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() {
let before = [
Column::new("Title"),
Column {
width: Width::Fixed,
priority: Priority::Optional,
kind: ColumnKind::Text,
..Column::new("Estimate")
},
];
let after = [
Column::new("Title"),
Column::new("Project"), Column {
width: Width::Fixed,
priority: Priority::Optional,
kind: ColumnKind::Text,
..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() {
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() {
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() {
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() {
assert_eq!(Arrangement::sidebar_content().share(), Some(Share::SIDEBAR));
assert_eq!(Arrangement::list_detail(false).share(), Some(Share::LIST));
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() {
assert_eq!(Measure::default(), Measure::Wide);
assert_eq!(Measure::Reading.as_str(), "reading");
}
#[test]
fn readiness_names_the_state_and_not_the_shimmer() {
assert_ne!(Readiness::Ready, Readiness::Pending);
}
#[test]
fn a_window_with_no_length_still_answers_what_it_can() {
let uncounted = Window::new(100, 50);
assert_eq!(uncounted.index(), Some(2));
assert_eq!(uncounted.windows(), None);
assert!(uncounted.has_before());
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() {
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);
assert_eq!(Window::new(900, 0).of(400).clamped().from, 399);
}
#[test]
fn a_window_past_the_end_clamps_inside_rather_than_vanishing() {
assert_eq!(Window::new(900, 50).of(400).clamped().from, 350);
assert_eq!(Window::new(900, 50).clamped().from, 900);
}
#[test]
fn a_carousel_frame_is_a_window_of_one() {
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() {
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());
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() {
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));
assert_eq!(Paging::more(500).of(400).remaining(), Some(0));
}
#[test]
fn a_fallback_is_authored_and_a_group_cannot_omit_it() {
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() {
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() {
assert_eq!(RowPart::Primary.priority(), Priority::Essential);
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);
assert_eq!(RowPart::Tokens.priority(), Priority::Secondary);
}
#[test]
fn a_run_is_one_line_unless_the_description_says_two() {
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() {
for flow in [Flow::Tight, Flow::Relaxed] {
assert!((1..=2).contains(&flow.lines()));
}
}
#[test]
fn an_awaiting_mark_is_indeterminate_until_something_is_measured() {
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));
}
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() {
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);
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() {
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);
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() {
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() {
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() {
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() {
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() {
assert!(Contrast::Low < Contrast::Standard);
assert!(Contrast::Standard < Contrast::High);
}
#[test]
fn the_groups_and_badges_have_one_spelling_each() {
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() {
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);
}
}
#[test]
fn a_column_kind_states_the_facts_every_renderer_reads() {
assert!(ColumnKind::Number.aligns_end() && ColumnKind::Actions.aligns_end());
assert!(
!ColumnKind::Date.wraps(),
"a date that wraps prints over its neighbour"
);
assert!(ColumnKind::Identifier.wraps() && ColumnKind::Identifier.monospace());
assert!(ColumnKind::Code.monospace() && !ColumnKind::Code.wraps());
assert!(ColumnKind::Date.tabular() && ColumnKind::Number.tabular());
assert_eq!(Column::new("x").kind, ColumnKind::Text);
}
#[test]
fn a_column_floor_is_declared_or_derived_and_always_on_the_ladder() {
assert_eq!(Column::new("Buyer").min(16).floor(), 16);
assert_eq!(Column::new("Buyer").min(15).floor(), 16);
let content = |name| Column {
width: Width::Content,
..Column::new(name)
};
let date = Column {
kind: ColumnKind::Date,
..content("Due")
};
assert_eq!(date.floor(), 10, "a date floors at what it holds");
assert_eq!(content("Description").floor(), 14, "a long heading wins");
let sorted = Column {
sortable: true,
..content("Description")
};
assert_eq!(sorted.floor(), 16);
assert_eq!(Column::new("Item").floor(), 16);
assert_eq!(content("Item").floor(), 8);
assert_eq!(Column::new("x").min(0).floor(), 2);
assert_eq!(Column::new("x").min(500).floor(), MIN_CEILING);
assert_eq!(MIN_CEILING % 2, 0, "the ceiling is itself a rung");
}
#[test]
fn only_the_kinds_that_cannot_truncate_hold_their_minimum() {
for kind in [
ColumnKind::Date,
ColumnKind::Number,
ColumnKind::Status,
ColumnKind::Actions,
] {
assert!(kind.holds_minimum(), "{kind:?} cut short is wrong");
}
for kind in [ColumnKind::Text, ColumnKind::Identifier, ColumnKind::Code] {
assert!(!kind.holds_minimum(), "{kind:?} ends in an ellipsis");
}
}