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::{
Affordance, AffordanceAxis, Arrangement, Awaiting, Bevel, Budget, Caret, CellPart, Change,
ColumnKind, ColumnRoom, Contrast, DATE_FORMAT, DATETIME_FORMAT, Depth, Edge, Extent, Facet,
FacetValue, Fallback, Family, FieldKind, Fill, Fit, Flow, Heading, Intent, Loading,
MIN_CEILING, Measure, Nesting, Notice, PageKind, Paging, Placement, Priority, Progress,
Readiness, Region, RowPart, Selecting, Selector, Share, Showing, Sizing, Sort, Span,
Standing, State, Strip, Syntax, ThemeVariant, Token, Tone, Track, Unit, Value, 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::*;
const KINDS: [FieldKind; 20] = [
FieldKind::Text,
FieldKind::Secret,
FieldKind::Number,
FieldKind::Range,
FieldKind::Interval,
FieldKind::Email,
FieldKind::Url,
FieldKind::Tel,
FieldKind::Date,
FieldKind::DateTime,
FieldKind::Textarea,
FieldKind::Rich,
FieldKind::Select,
FieldKind::Radio,
FieldKind::Checklist,
FieldKind::Checkbox,
FieldKind::File,
FieldKind::Theme,
FieldKind::Density,
FieldKind::Hidden,
];
const REGIONS: [Region<'static>; 12] = [
Region::Band,
Region::Sidebar,
Region::Pane,
Region::Group,
Region::Plate,
Region::Split,
Region::Columns,
Region::TabGroup,
Region::Modal,
Region::Handover { name: "day-plan" },
Region::Ceded { name: "waveform" },
Region::Widget { name: "carousel" },
];
fn kinds_where(test: impl Fn(FieldKind) -> bool) -> Vec<FieldKind> {
KINDS.into_iter().filter(|kind| test(*kind)).collect()
}
fn regions_where(test: impl Fn(Region<'static>) -> bool) -> Vec<Region<'static>> {
REGIONS.into_iter().filter(|region| test(*region)).collect()
}
#[test]
fn the_variant_lists_hold_every_member_once_in_order() {
const fn kind_at(kind: FieldKind) -> usize {
match kind {
FieldKind::Text => 0,
FieldKind::Secret => 1,
FieldKind::Number => 2,
FieldKind::Range => 3,
FieldKind::Interval => 4,
FieldKind::Email => 5,
FieldKind::Url => 6,
FieldKind::Tel => 7,
FieldKind::Date => 8,
FieldKind::DateTime => 9,
FieldKind::Textarea => 10,
FieldKind::Rich => 11,
FieldKind::Select => 12,
FieldKind::Radio => 13,
FieldKind::Checklist => 14,
FieldKind::Checkbox => 15,
FieldKind::File => 16,
FieldKind::Theme => 17,
FieldKind::Density => 18,
FieldKind::Hidden => 19,
}
}
const fn region_at(region: Region<'_>) -> usize {
match region {
Region::Band => 0,
Region::Sidebar => 1,
Region::Pane => 2,
Region::Group => 3,
Region::Plate => 4,
Region::Split => 5,
Region::Columns => 6,
Region::TabGroup => 7,
Region::Modal => 8,
Region::Handover { .. } => 9,
Region::Ceded { .. } => 10,
Region::Widget { .. } => 11,
}
}
for (at, kind) in KINDS.into_iter().enumerate() {
assert_eq!(kind_at(kind), at, "{kind:?}");
}
for (at, region) in REGIONS.into_iter().enumerate() {
assert_eq!(region_at(region), at, "{region:?}");
}
}
#[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_eq!(
kinds_where(FieldKind::measurable),
[FieldKind::Number, FieldKind::Range, FieldKind::Interval]
);
}
#[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!(!Standing::PrunedAbove.is_picked());
assert!(!Standing::PrunedAbove.in_force());
assert!(Standing::PrunedAbove.pruned_out());
assert!(Standing::Pruned.pruned_out());
assert!(!Standing::Inherited.pruned_out());
assert_eq!(Standing::PrunedAbove.intent(), Standing::Pruned.intent());
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() {
assert_eq!(kinds_where(FieldKind::takes_files), [FieldKind::File]);
}
#[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_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_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 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-disabled");
}
#[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 neutral_is_content_not_muted_content() {
assert_eq!(Tone::Neutral.token(), "content");
assert!(!Token::Badge.interactive());
assert_eq!(State::Disabled.token(), "content-disabled");
}
#[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 heat_bands_and_clamps() {
assert_eq!(Heat(0).token(), "heat-0");
assert_eq!(Heat(3).token(), "heat-3");
assert_eq!(Heat(9).token(), "heat-4");
assert_eq!(Heat(u8::MAX).token(), "heat-4");
}
#[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_eq!(
kinds_where(FieldKind::temporal),
[FieldKind::Date, FieldKind::DateTime]
);
}
#[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_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_strip_that_runs_down_stacks_its_tabs_and_one_that_runs_across_does_not() {
use crate::Strip;
assert!(Strip::Down.runs_down());
assert!(!Strip::Across.runs_down());
let named = |strip: Strip| match strip {
Strip::Across => "across",
Strip::Down => "down",
};
assert_ne!(named(Strip::Across), named(Strip::Down));
}
#[test]
fn a_pane_is_looked_into_and_a_band_is_not() {
assert_eq!(regions_where(|r| r.depth() == Depth::Well), [Region::Pane]);
assert_eq!(
regions_where(|r| r.depth() == Depth::Raised),
[Region::Plate, Region::Modal]
);
assert_eq!(
regions_where(|r| r.depth() != Depth::Flat),
[Region::Pane, Region::Plate, Region::Modal]
);
}
#[test]
fn only_the_two_opaque_regions_go_undescribed() {
assert_eq!(
regions_where(|r| !r.described()),
[
Region::Handover { name: "day-plan" },
Region::Ceded { name: "waveform" }
]
);
}
#[test]
fn a_region_nobody_converted_yet_is_not_a_region_the_app_gave_up_on() {
assert_eq!(
regions_where(Region::owed),
[Region::Handover { name: "day-plan" }]
);
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_eq!(Region::Group.depth(), Depth::Flat);
assert_eq!(Region::Group.name(), None);
}
#[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")
);
assert_eq!(
regions_where(|r| r.name().is_some()),
[
Region::Handover { name: "day-plan" },
Region::Ceded { name: "waveform" },
Region::Widget { name: "carousel" }
]
);
}
#[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() {
assert!(FieldKind::Secret.confidential());
assert!(FieldKind::Secret.visible());
assert_eq!(kinds_where(|k| !k.visible()), [FieldKind::Hidden]);
assert_eq!(kinds_where(FieldKind::confidential), [FieldKind::Secret]);
assert_eq!(kinds_where(FieldKind::labels_itself), [FieldKind::Checkbox]);
}
#[test]
fn exactly_the_option_taking_kinds_say_so() {
assert_eq!(
kinds_where(FieldKind::offers_options),
[FieldKind::Select, FieldKind::Radio, FieldKind::Checklist]
);
}
#[test]
fn a_checklist_is_one_question_whose_answer_is_a_set() {
assert_eq!(
kinds_where(FieldKind::takes_several),
[FieldKind::Checklist]
);
assert!(!FieldKind::Checklist.labels_itself());
}
#[test]
fn only_the_density_picker_is_held_by_the_device() {
assert_eq!(kinds_where(FieldKind::held_by_device), [FieldKind::Density]);
assert!(!FieldKind::Density.offers_options());
assert!(!FieldKind::Density.offers_themes());
}
#[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 columns_drop_by_priority_and_never_by_position() {
let cols = [
ColumnRoom {
width: Width::Fill,
priority: Priority::Essential,
..ColumnRoom::new("Title")
},
ColumnRoom {
width: Width::Fixed,
priority: Priority::Secondary,
..ColumnRoom::new("Due")
},
ColumnRoom {
width: Width::Fixed,
priority: Priority::Optional,
..ColumnRoom::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.heading)
.collect();
assert_eq!(kept, ["Title", "Due"]);
let kept: Vec<_> = cols
.iter()
.filter(|c| c.kept_at(Priority::Essential))
.map(|c| c.heading)
.collect();
assert_eq!(kept, ["Title"]);
}
#[test]
fn inserting_a_column_does_not_move_what_gets_dropped() {
let estimate = ColumnRoom {
width: Width::Fixed,
priority: Priority::Optional,
..ColumnRoom::new("Estimate")
};
let before = [ColumnRoom::new("Title"), estimate];
let after = [
ColumnRoom::new("Title"),
ColumnRoom::new("Project"), estimate,
];
fn dropped<'a>(cols: &[ColumnRoom<'a>]) -> Vec<&'a str> {
cols.iter()
.filter(|c| !c.kept_at(Priority::Secondary))
.map(|c| c.heading)
.collect()
}
assert_eq!(dropped(&before), ["Estimate"]);
assert_eq!(dropped(&after), ["Estimate"]);
}
#[test]
fn a_table_with_no_essential_column_never_drops_to_nothing() {
let rank = |name, priority| ColumnRoom {
priority,
..ColumnRoom::new(name)
};
let cols = [
rank("Optional", Priority::Optional),
rank("Secondary", Priority::Secondary),
];
assert_eq!(
ColumnRoom::cutoff_for(&cols, |_| false),
Priority::Secondary
);
let with_essential = [
rank("Optional", Priority::Optional),
ColumnRoom::new("Essential"),
];
assert_eq!(
ColumnRoom::cutoff_for(&with_essential, |_| false),
Priority::Essential
);
}
#[test]
fn a_multibyte_heading_floors_in_characters_not_bytes() {
let fixed = |heading| ColumnRoom {
width: Width::Fixed,
..ColumnRoom::new(heading)
};
assert_eq!(fixed("Статус").floor(), fixed("Status").floor());
assert_eq!(fixed("Статус").floor(), 8);
}
#[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(), 60);
assert_eq!(Share::LIST.of(100), 60);
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), 0);
}
#[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 an_empty_sequence_has_no_window() {
let none = Window::new(0, 10).of(0);
assert_eq!(none.index(), None);
assert_eq!(none.windows(), Some(0));
assert_eq!(Paging::pages(0, 10).of(0).page(), None);
assert_eq!(Paging::pages(0, 10).of(0).pages_total(), Some(0));
}
#[test]
fn a_window_past_the_end_numbers_no_page() {
let past = crate::Paging::pages(20, 10).of(20);
assert_eq!(past.page(), None);
assert_eq!(past.pages_total(), Some(2));
assert!(past.has_previous());
assert_eq!(Window::new(5, 10).of(5).index(), None);
assert_eq!(Window::new(20, 10).of(20).clamped().index(), Some(1));
assert_eq!(crate::Paging::pages(19, 10).of(20).page(), Some(3));
}
#[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));
}
#[test]
fn a_wait_reading_is_done_over_total_and_the_whole_seconds_taken() {
use crate::progress_reading;
use std::time::Duration;
assert_eq!(progress_reading(0, 0, None), "0/0");
assert_eq!(progress_reading(50, 40, None), "50/40");
assert_eq!(
progress_reading(17_825_792, 41_943_040, Some(Duration::from_millis(4_999))),
"17825792/41943040 4s"
);
assert_eq!(Progress::default().delivered, None);
}
#[test]
fn every_kind_but_theme_offers_no_themes() {
assert_eq!(kinds_where(FieldKind::offers_themes), [FieldKind::Theme]);
}
#[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!(ColumnRoom::new("x").kind, ColumnKind::Text);
}
#[test]
fn a_column_floor_is_declared_or_derived_and_always_on_the_ladder() {
let declared = |heading, ch| ColumnRoom {
min: Some(ch),
..ColumnRoom::new(heading)
};
assert_eq!(declared("Buyer", 16).floor(), 16);
assert_eq!(declared("Buyer", 15).floor(), 16);
let content = |heading| ColumnRoom {
width: Width::Content,
..ColumnRoom::new(heading)
};
let date = ColumnRoom {
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 = ColumnRoom {
caret: true,
..content("Description")
};
assert_eq!(sorted.floor(), 16);
assert_eq!(ColumnRoom::new("Item").floor(), 16);
assert_eq!(content("Item").floor(), 8);
assert_eq!(declared("x", 0).floor(), 2);
assert_eq!(declared("x", 500).floor(), MIN_CEILING);
assert_eq!(MIN_CEILING % 2, 0, "the ceiling is itself a rung");
}
#[test]
fn every_kind_floors_wide_enough_to_be_recognised() {
for kind in [
ColumnKind::Text,
ColumnKind::Identifier,
ColumnKind::Date,
ColumnKind::Number,
ColumnKind::Code,
ColumnKind::Status,
ColumnKind::Actions,
] {
assert!(
kind.floor() >= 6,
"{kind:?} floors at {} ch, which is a fragment rather than a column",
kind.floor()
);
assert!(
kind.floor() <= MIN_CEILING,
"{kind:?} floors above the ladder the webview can carry"
);
}
assert!(ColumnKind::Date.floor() > ColumnKind::Number.floor());
}
#[test]
fn a_span_and_a_placement_at_the_end_of_the_axis_do_not_overflow() {
let span = crate::Span::new(u16::MAX, 3);
assert!(span.to() > span.from());
let late = crate::Placement::new(1000, u16::MAX);
assert_eq!(late.end(), u16::MAX);
let edge = crate::Placement::new(u16::MAX, 10);
assert_eq!(edge.end(), u16::MAX);
assert!(late.overlaps(edge));
}
#[test]
fn a_column_floor_declared_past_the_ceiling_is_held_under_it() {
let column = crate::ColumnRoom {
min: Some(u16::MAX),
..crate::ColumnRoom::new("Wide")
};
assert_eq!(column.floor(), crate::MIN_CEILING);
}
#[test]
fn a_share_rounds_to_nearest_and_never_invents_width() {
let share = crate::Share::percent(21);
assert_eq!(share.of(10), 2);
assert_eq!(crate::Share::percent(25).of(10), 3);
assert_eq!(share.of(0), 0);
assert_eq!(crate::Share::percent(5).of(10), 1);
assert_eq!(crate::Share::percent(5).of(4), 1);
assert!(crate::Share::percent(95).of(7) <= 7);
}
#[test]
fn a_misaligned_page_counts_the_short_page_before_it() {
let paging = crate::Paging::pages(5, 10).of(20);
assert_eq!(paging.page(), Some(2));
assert_eq!(paging.pages_total(), Some(3));
assert!(paging.has_previous());
let aligned = crate::Paging::pages(10, 10).of(20);
assert_eq!(aligned.page(), Some(2));
assert_eq!(aligned.pages_total(), Some(2));
let first = crate::Paging::pages(0, 10).of(25);
assert_eq!((first.page(), first.pages_total()), (Some(1), Some(3)));
}
fn three_floored_columns() -> [ColumnRoom<'static>; 3] {
let floored = |heading, ch, priority| ColumnRoom {
min: Some(ch),
priority,
..ColumnRoom::new(heading)
};
[
floored("name", 20, Priority::Essential),
floored("size", 8, Priority::Optional),
floored("owner", 10, Priority::Secondary),
]
}
#[test]
fn a_budget_counts_only_the_columns_kept_at_its_cutoff() {
let columns = three_floored_columns();
let all = ColumnRoom::budget(&columns, Priority::Optional);
assert_eq!((all.columns, all.floors), (3, 38));
let kept = ColumnRoom::budget(&columns, Priority::Essential);
assert_eq!((kept.columns, kept.floors), (1, 20));
assert!((all.width(1.0, 2.0) - (38.0 + 8.0)).abs() < f32::EPSILON);
}
#[test]
fn the_cutoff_rises_only_until_the_columns_fit() {
let columns = three_floored_columns();
let at = |width: f32| ColumnRoom::cutoff_for(&columns, |b| b.width(1.0, 1.0) <= width);
assert_eq!(at(100.0), Priority::Optional);
assert_eq!(at(33.0), Priority::Secondary);
assert_eq!(at(22.0), Priority::Essential);
assert_eq!(at(1.0), Priority::Essential);
assert_eq!(Priority::RAISING.last(), Some(&Priority::Essential));
}
#[test]
fn sizing_answers_by_name_and_falls_back() {
let sizing = crate::Sizing::new(&[("due", 12u16)], 8);
assert_eq!(sizing.length_for("due"), 12);
assert_eq!(sizing.length_for("title"), 8);
}
#[test]
fn a_growing_set_has_no_page_to_number() {
assert_eq!(Paging::more(150).of(400).page(), None);
assert_eq!(
Paging::pages(50, 50).of(400).kind,
crate::PageKind::Numbered
);
let window = Window::new(10, 5).of(40);
assert_eq!(
(window.from(), window.count(), window.length()),
(10, 5, Some(40))
);
}
#[test]
fn a_plate_is_a_group_lifted_rather_than_a_pane_turned_over() {
assert_eq!(Region::Plate.depth(), Depth::Raised);
assert_ne!(Region::Plate.depth(), Region::Pane.depth());
assert_ne!(Region::Plate, Region::Group);
assert!(Region::Plate.described());
assert!(!Region::Plate.owed());
assert_eq!(Region::Plate.name(), None);
}
#[test]
fn the_density_picker_offers_detection_first_and_each_density_once() {
use makeover_geometry::Density;
let offered: Vec<_> = crate::DENSITY_CHOICES.iter().map(|(d, _)| *d).collect();
assert_eq!(
offered,
[None, Some(Density::Pointer), Some(Density::Touch)]
);
let labels: std::collections::HashSet<_> =
crate::DENSITY_CHOICES.iter().map(|(_, l)| *l).collect();
assert_eq!(labels.len(), 3, "two entries share a name");
}
#[test]
fn a_media_type_names_its_family_by_its_first_segment_in_any_case() {
for family in [Family::Image, Family::Audio, Family::Video] {
assert_eq!(Family::of_type(family.wildcard()), Some(family));
}
assert_eq!(Family::of_type("AUDIO/x-wav"), Some(Family::Audio));
assert_eq!(Family::of_type("image/png"), Some(Family::Image));
assert_eq!(Family::of_type("text/plain"), None);
assert_eq!(Family::of_type("image"), None);
assert_eq!(Family::of_type("/png"), None);
}
#[test]
fn a_heading_carries_its_direction_the_idle_mark_or_nothing() {
let (up, down) = (Sort::Ascending.glyph(), Sort::Descending.glyph());
for offers in [false, true] {
assert_eq!(
Caret::of(Some(Sort::Descending), offers),
Caret::Sorted(Sort::Descending)
);
}
assert_eq!(Caret::of(None, true), Caret::Idle);
assert_eq!(Caret::of(None, false), Caret::None);
assert_eq!(
Caret::Sorted(Sort::Ascending).label("size", up, down),
"SIZE \u{25B2}"
);
assert_eq!(
Caret::Sorted(Sort::Descending).label("size", up, down),
"SIZE \u{25BC}"
);
assert_eq!(Caret::None.label("size", up, down), "SIZE");
assert_eq!(
Caret::Idle.label("size", up, down),
Caret::Sorted(Sort::Ascending).label("size", up, down)
);
}
#[test]
fn a_picture_column_floors_wider_than_prose_and_tiles_only_out_of_room_by_default() {
assert!(ColumnKind::Picture.floor() > ColumnKind::Identifier.floor());
assert!(!ColumnKind::Picture.wraps());
assert!(!ColumnKind::Picture.aligns_end());
assert_eq!(Tiling::default(), Tiling::Narrow);
}