use super::*;
use crate::widget::{WidgetStyle, chart};
use makeover_layout::{Bar, Chart};
fn palette(well: Color32) -> Palette {
Palette {
page: Color32::from_rgb(1, 1, 1),
raised: Color32::from_rgb(2, 2, 2),
overlay: Color32::from_rgb(3, 3, 3),
well,
sunken: Color32::from_rgb(4, 4, 4),
bevel_light: Color32::WHITE,
bevel_dark: Color32::BLACK,
elevation: Color32::from_black_alpha(46),
content: Color32::from_rgb(5, 5, 5),
content_secondary: Color32::from_rgb(55, 55, 55),
content_muted: Color32::from_rgb(6, 6, 6),
action: Color32::from_rgb(7, 7, 7),
content_on_action: Color32::from_rgb(250, 250, 250),
danger: Color32::from_rgb(8, 8, 8),
success: Color32::from_rgb(9, 9, 9),
warning: Color32::from_rgb(10, 10, 10),
info: Color32::from_rgb(11, 11, 11),
border: Color32::from_rgb(200, 200, 200),
info_surface: Color32::from_rgb(201, 201, 201),
success_surface: Color32::from_rgb(202, 202, 202),
warning_surface: Color32::from_rgb(203, 203, 203),
danger_surface: Color32::from_rgb(204, 204, 204),
row_stripe: Color32::from_rgb(205, 205, 205),
row_hover: Color32::from_rgb(206, 206, 206),
row_rule: Color32::from_rgb(207, 207, 207),
row_selected: Color32::from_rgb(208, 208, 208),
}
}
#[test]
fn a_unit_is_drawn_only_where_the_kind_is_a_quantity() {
let ranged = makeover_layout::Field {
unit: Some("s"),
..makeover_layout::Field::range("attack", "Attack", "0", "5")
};
assert_eq!(unit_of(&ranged), Some("s"));
let typed = makeover_layout::Field {
unit: Some("ms"),
..makeover_layout::Field::new(makeover_layout::FieldKind::Number, "fade", "Fade")
};
assert_eq!(unit_of(&typed), Some("ms"));
let worded = makeover_layout::Field {
unit: Some("s"),
..makeover_layout::Field::new(makeover_layout::FieldKind::Text, "name", "Name")
};
assert_eq!(unit_of(&worded), None);
let bare = makeover_layout::Field::range("attack", "Attack", "0", "5");
assert_eq!(unit_of(&bare), None);
}
#[test]
fn the_cast_hands_egui_the_themes_tone() {
let p = palette(Color32::from_rgb(9, 9, 9));
let cast = p.cast();
assert_eq!(cast.color, p.elevation);
assert!(cast.blur > 0, "a cast shadow is soft");
assert_eq!(cast.offset, [0, 2], "it falls downward and only a little");
}
#[test]
fn a_well_resolves_to_its_own_token() {
let w = Color32::from_rgb(9, 9, 9);
let p = palette(w);
assert_eq!(p.fill(Fill::Well), Some(w));
assert_ne!(p.fill(Fill::Well), Some(p.page));
}
#[test]
fn every_intent_is_a_plain_lookup() {
let p = palette(Color32::from_rgb(9, 9, 9));
assert_eq!(p.fill(Fill::Page), Some(p.page));
assert_eq!(p.fill(Fill::Raised), Some(p.raised));
assert_eq!(p.fill(Fill::Overlay), Some(p.overlay));
}
#[test]
fn sunken_is_neither_the_well_nor_the_page() {
let p = palette(Color32::from_rgb(9, 9, 9));
assert_eq!(p.fill(Fill::Sunken), Some(p.sunken));
assert_ne!(p.fill(Fill::Sunken), p.fill(Fill::Well));
assert_ne!(p.fill(Fill::Sunken), p.fill(Fill::Page));
}
#[test]
fn a_raised_region_never_resolves_to_the_well_fill() {
let p = palette(Color32::from_rgb(9, 9, 9));
let raised = Depth::Raised.fill().and_then(|f| p.fill(f));
let well = Depth::Well.fill().and_then(|f| p.fill(f));
assert_eq!(raised, Some(p.raised));
assert_ne!(raised, well);
}
#[test]
fn an_overlay_is_cast_onto_the_page_and_takes_no_edge() {
let p = palette(Color32::from_rgb(9, 9, 9));
assert_eq!(
Depth::Overlay.fill().and_then(|f| p.fill(f)),
Some(p.overlay)
);
assert_eq!(Depth::Overlay.bevel(), None);
assert_eq!(p.cast().color, p.elevation);
}
#[test]
fn the_lit_edge_swaps_when_a_card_is_pressed() {
let p = palette(Color32::from_rgb(9, 9, 9));
let (tl, _) = Depth::Raised.bevel().unwrap().edges();
let (ptl, _) = Depth::Raised.pressed().bevel().unwrap().edges();
assert_eq!(p.edge(tl), p.bevel_light);
assert_eq!(p.edge(ptl), p.bevel_dark);
}
#[test]
fn flat_asks_for_neither_fill_nor_edge() {
assert!(Depth::Flat.fill().is_none());
assert!(Depth::Flat.bevel().is_none());
}
#[test]
fn a_select_keeps_a_value_none_of_its_options_carries() {
let options = [
Choice::plain("1"),
Choice::plain("3"),
Choice::plain("7"),
Choice::plain("14"),
];
assert_eq!(shown_label(&options, "10"), "10");
let spelled = [Choice::new("7", "One week")];
assert_eq!(shown_label(&spelled, "7"), "One week");
}
#[test]
fn an_unanswered_chooser_reads_its_ghost_text_and_reads_it_muted() {
let p = palette(Color32::from_rgb(9, 9, 9));
let options = [Choice::new("sp404", "SP-404")];
let field = Field {
placeholder: Some("Select device..."),
..Field::select("device", "Conform for device", &options)
};
assert_eq!(
chosen_text(&field, "", &p),
("Select device...", p.content_muted),
"ghost text is not an answer, so it takes the tone the typed kinds' ghost text does"
);
assert_eq!(chosen_text(&field, "sp404", &p), ("SP-404", p.content));
}
#[test]
fn a_wrong_answer_is_not_an_absent_one() {
let p = palette(Color32::from_rgb(9, 9, 9));
let options = [Choice::plain("1"), Choice::plain("7")];
let field = Field {
placeholder: Some("Pick one"),
..Field::select("retention", "Keep backups for", &options)
};
assert_eq!(chosen_text(&field, "10", &p), ("10", p.content));
}
#[test]
fn a_chooser_with_no_ghost_text_is_unchanged() {
let p = palette(Color32::from_rgb(9, 9, 9));
let options = [Choice::plain("1")];
let field = Field::select("retention", "Keep backups for", &options);
assert_eq!(chosen_text(&field, "", &p), ("", p.content));
}
#[test]
fn a_range_is_slid_and_a_number_is_typed_into() {
assert_eq!(control_shape(FieldKind::Range), Control::Slid);
assert_eq!(control_shape(FieldKind::Number), Control::Typed);
}
#[test]
fn a_range_missing_an_end_falls_back_to_a_well() {
let whole = Field::range("review", "Review above", "0", "1");
assert_eq!(shape_of(&whole), Control::Slid);
let half = Field {
max: Some("1"),
..Field::new(FieldKind::Range, "review", "Review above")
};
assert_eq!(shape_of(&half), Control::Typed);
assert_eq!(extent(&half), None);
let dated = Field::range("when", "When", "2026-08-01", "2026-08-31");
assert_eq!(extent(&dated), None);
}
#[test]
fn an_interval_is_its_own_shape_and_not_two_numbers() {
assert_eq!(control_shape(FieldKind::Interval), Control::Spanned);
assert_eq!(shape_of(&Field::interval("a", "b", "A")), Control::Spanned);
let axis = Field {
min: Some("0"),
max: Some("300"),
..Field::interval("bpm_min", "bpm_max", "BPM")
};
assert_eq!(shape_of(&axis), Control::Spanned);
}
#[test]
fn an_empty_end_reads_as_the_bound_it_stands_for() {
let axis = Axis {
extent: Some(0.0..=300.0),
step: Some("1"),
unit: Some("BPM"),
};
assert!((axis.edge(Bound::Low) - 0.0).abs() < f64::EPSILON);
assert!((axis.edge(Bound::High) - 300.0).abs() < f64::EPSILON);
let open = Axis {
extent: None,
step: None,
unit: None,
};
assert!((open.edge(Bound::Low) - 0.0).abs() < f64::EPSILON);
assert!((open.edge(Bound::High) - 0.0).abs() < f64::EPSILON);
}
#[test]
fn the_step_decides_how_a_dragged_value_is_written_back() {
assert_eq!(decimals(None), None);
assert_eq!(decimals(Some("1")), Some(0));
assert_eq!(decimals(Some("0.01")), Some(2));
assert_eq!(decimals(Some("0.10")), Some(1));
}
#[test]
fn an_unavailable_option_is_drawn_muted_rather_than_dropped() {
let p = palette(Color32::from_rgb(9, 9, 9));
let options = [
Choice::new("chromatic", "Chromatic"),
Choice::new("multi", "Multi-sample").unless("Drop a second sample."),
];
assert!(options[0].available());
assert!(!options[1].available());
assert_eq!(
option_color("chromatic", options[0].value, &p),
p.content,
"the chosen option is the emphasised thing"
);
assert_eq!(
option_color("chromatic", options[1].value, &p),
p.content_secondary,
"and `option_color` never mutes: the unavailable path is what does"
);
}
#[test]
fn a_closed_chooser_carries_both_of_an_options_extra_lines_in_its_row() {
let plain = Choice::new("24", "Small Files");
assert_eq!(combo_row(&plain), "Small Files");
let detailed = plain.detailing("$24/mo. Fits audio, plugins, binaries.");
assert_eq!(
combo_row(&detailed),
"Small Files $24/mo. Fits audio, plugins, binaries."
);
let both = detailed.unless("Sold out.");
assert_eq!(
combo_row(&both),
"Small Files $24/mo. Fits audio, plugins, binaries. Sold out."
);
assert_eq!(
combo_row(&plain.unless("Sold out.")),
"Small Files Sold out.",
"the reason still reads the way it did before the detail existed"
);
}
#[test]
fn only_a_required_field_is_marked() {
let style = FieldStyle::default();
let plain = Field::new(FieldKind::Text, "title", "Title");
assert_eq!(label_text(&plain, &style), "Title");
let required = Field {
required: true,
..plain
};
assert_eq!(label_text(&required, &style), "Title *");
let house = FieldStyle {
required_marker: "(required)",
..style
};
assert_eq!(label_text(&required, &house), "Title (required)");
}
#[test]
fn a_select_and_a_checkbox_are_pressed_and_everything_else_is_typed_into() {
assert_eq!(control_shape(FieldKind::Select), Control::Chosen);
assert_eq!(control_shape(FieldKind::Theme), Control::Themed);
assert_eq!(control_shape(FieldKind::Radio), Control::Listed);
assert_eq!(control_shape(FieldKind::Checkbox), Control::Toggled);
for k in [
FieldKind::Text,
FieldKind::Secret,
FieldKind::Number,
FieldKind::Email,
FieldKind::Url,
FieldKind::Tel,
FieldKind::Textarea,
FieldKind::Rich,
] {
assert_eq!(control_shape(k), Control::Typed, "{k:?} is typed into");
}
assert!(FieldKind::Rich.multiline());
assert!(FieldKind::Textarea.multiline());
assert!(!FieldKind::Text.multiline());
}
#[test]
fn the_two_option_taking_kinds_are_drawn_differently_on_purpose() {
assert!(FieldKind::Select.offers_options());
assert!(FieldKind::Radio.offers_options());
assert_ne!(
control_shape(FieldKind::Select),
control_shape(FieldKind::Radio)
);
}
#[test]
fn a_checklist_is_ticked_rather_than_listed_or_toggled() {
assert!(FieldKind::Checklist.offers_options());
assert_eq!(control_shape(FieldKind::Checklist), Control::Ticked);
assert_ne!(
control_shape(FieldKind::Checklist),
control_shape(FieldKind::Radio)
);
assert_ne!(
control_shape(FieldKind::Checklist),
control_shape(FieldKind::Checkbox)
);
}
#[test]
fn an_unchosen_option_is_secondary_and_never_muted() {
let p = palette(Color32::from_rgb(4, 4, 4));
assert_eq!(option_color("wav", "wav", &p), p.content);
assert_eq!(option_color("wav", "aiff", &p), p.content_secondary);
assert_ne!(option_color("wav", "aiff", &p), p.content_muted);
}
fn announced(draw: impl FnMut(&mut egui::Ui)) -> Vec<(egui::accesskit::Role, String)> {
let ctx = egui::Context::default();
ctx.enable_accesskit();
let mut draw = draw;
let input = || egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(800.0, 600.0),
)),
..Default::default()
};
let _ = ctx.run_ui(input(), &mut draw);
let out = ctx.run_ui(input(), &mut draw);
let update = out
.platform_output
.accesskit_update
.expect("accesskit is on, so a tree was built");
let by_id: std::collections::HashMap<_, _> = update.nodes.iter().cloned().collect();
update
.nodes
.iter()
.map(|(_, node)| {
let named = node.label().map(str::to_owned).or_else(|| {
node.labelled_by()
.iter()
.find_map(|id| by_id.get(id))
.and_then(|by| by.label().or_else(|| by.value()).map(str::to_owned))
});
(node.role(), named.unwrap_or_default())
})
.collect()
}
#[test]
fn a_fields_label_names_its_control_rather_than_sitting_beside_it() {
let f = Field::new(FieldKind::Text, "name", "Vault name");
let p = palette(Color32::from_rgb(9, 9, 9));
let drawn = announced(|ui| {
let mut text = String::new();
field(
ui,
&f,
Filling::Text(&mut text),
None,
&p,
&FieldStyle::default(),
);
});
let box_ = drawn
.iter()
.find(|(role, _)| *role == egui::accesskit::Role::TextInput)
.expect("a text field draws a text input");
assert_eq!(
box_.1, "Vault name",
"the box is announced by the question rather than unnamed: {drawn:?}"
);
}
#[test]
fn a_prefilled_box_is_still_announced_by_its_question() {
let f = Field::new(FieldKind::Text, "name", "New name");
let p = palette(Color32::from_rgb(9, 9, 9));
let drawn = announced(|ui| {
let mut text = String::from("Drums");
field(
ui,
&f,
Filling::Text(&mut text),
None,
&p,
&FieldStyle::default(),
);
});
let box_ = drawn
.iter()
.find(|(role, _)| *role == egui::accesskit::Role::TextInput)
.expect("a text field draws a text input");
assert_eq!(box_.1, "New name", "{drawn:?}");
}
#[test]
fn both_ends_of_an_interval_are_named_by_the_one_question() {
let f = Field::new(FieldKind::Interval, "bpm", "BPM Range");
let p = palette(Color32::from_rgb(9, 9, 9));
let drawn = announced(|ui| {
let mut lower = String::from("90");
let mut upper = String::from("300");
field(
ui,
&f,
Filling::Between {
lower: &mut lower,
upper: &mut upper,
},
None,
&p,
&FieldStyle::default(),
);
});
let ends: Vec<_> = drawn
.iter()
.filter(|(role, _)| *role == egui::accesskit::Role::SpinButton)
.collect();
assert_eq!(ends.len(), 2, "an interval draws two boxes: {drawn:?}");
for end in ends {
assert_eq!(end.1, "BPM Range", "{drawn:?}");
}
}
#[test]
fn a_checkbox_keeps_naming_itself() {
let f = Field::new(FieldKind::Checkbox, "loop", "Loop playback");
let p = palette(Color32::from_rgb(9, 9, 9));
let drawn = announced(|ui| {
let mut ticked = false;
field(
ui,
&f,
Filling::On(&mut ticked),
None,
&p,
&FieldStyle::default(),
);
});
assert!(
drawn
.iter()
.any(|(role, name)| *role == egui::accesskit::Role::CheckBox
&& name == "Loop playback"),
"{drawn:?}"
);
}
#[test]
fn a_hidden_field_draws_nothing_and_answers_nothing() {
let f = Field::new(FieldKind::Hidden, "id", "Id");
let p = palette(Color32::from_rgb(9, 9, 9));
egui::__run_test_ui(|ui| {
let drawn = field(ui, &f, Filling::Absent, None, &p, &FieldStyle::default());
assert!(drawn.is_none());
});
}
#[test]
fn a_disabled_field_stops_answering_and_an_unstated_one_does_not() {
let f = Field::new(FieldKind::Text, "title", "Title");
let p = palette(Color32::from_rgb(9, 9, 9));
let style = FieldStyle::default();
egui::__run_test_ui(|ui| {
let mut text = String::from("x");
let disabled = field(
ui,
&f,
Filling::Text(&mut text),
Some(State::Disabled),
&p,
&style,
)
.unwrap();
assert!(!disabled.enabled());
let mut text = String::from("x");
let plain = field(ui, &f, Filling::Text(&mut text), None, &p, &style).unwrap();
assert!(plain.enabled(), "an unstated field still answers");
});
}
#[test]
fn a_field_described_one_way_and_filled_another_is_drawn_inert() {
let f = Field::new(FieldKind::Checkbox, "done", "Done");
let p = palette(Color32::from_rgb(9, 9, 9));
let mut text = String::from("untouched");
egui::__run_test_ui(|ui| {
let drawn = field(
ui,
&f,
Filling::Text(&mut text),
None,
&p,
&FieldStyle::default(),
);
assert!(drawn.is_some());
});
assert_eq!(text, "untouched");
}
#[test]
fn the_disclosure_belongs_to_the_form_and_not_to_the_field() {
let fields = [
Field::new(FieldKind::Text, "title", "Title"),
Field {
extended: true,
..Field::new(FieldKind::Text, "notes", "Notes")
},
];
let style = FieldStyle::default();
let mut closed = Vec::new();
egui::__run_test_ui(|ui| {
group(ui, &fields, false, &style, |_, f| closed.push(f.name));
});
assert_eq!(closed, ["title"]);
let mut open = Vec::new();
egui::__run_test_ui(|ui| {
group(ui, &fields, true, &style, |_, f| open.push(f.name));
});
assert_eq!(open, ["title", "notes"]);
}
#[test]
fn the_default_frame_is_square_and_one_point() {
let d = FrameStyle::default();
assert_eq!(d.radius, CornerRadius::ZERO);
assert_eq!(d.margin, Margin::ZERO);
assert!((d.stroke - 1.0).abs() < f32::EPSILON);
}
#[test]
fn a_theme_picker_reads_its_chosen_theme_by_name() {
const THEMES: &[makeover_layout::ThemeChoice<'_>] = &[makeover_layout::ThemeChoice::new(
"carbonfox",
"Carbonfox",
ThemeVariant::Dark,
makeover_layout::Contrast::High,
)];
let field =
Field::theme("theme", "Theme", THEMES).following(Choice::new("system", "Follow System"));
assert_eq!(themed_text(&field, "carbonfox"), "Carbonfox");
assert_eq!(themed_text(&field, "system"), "Follow System");
assert_eq!(themed_text(&field, "gone"), "gone");
}
#[test]
fn the_columns_reach_their_share_of_the_height() {
let p = palette(Color32::from_rgb(9, 9, 9));
let style = WidgetStyle::default();
let chart_of = Chart::new(20);
let bars = [
Bar::at("Mar 3").of(20),
Bar::at("Mar 4").of(10),
Bar::at("Mar 5").of(0),
];
egui::__run_test_ui(|ui| {
chart(ui, &chart_of, &bars, &p, &style);
});
assert!((bars[0].fraction(&chart_of) - 1.0).abs() < f32::EPSILON);
assert!((bars[1].fraction(&chart_of) - 0.5).abs() < f32::EPSILON);
assert!(bars[2].fraction(&chart_of).abs() < f32::EPSILON);
}
#[test]
fn an_empty_axis_paints_flat_bars() {
let p = palette(Color32::from_rgb(9, 9, 9));
let empty = Chart::new(0);
let bars = [Bar::at("Mar 3").of(5)];
egui::__run_test_ui(|ui| {
chart(ui, &empty, &bars, &p, &WidgetStyle::default());
});
assert!(bars[0].fraction(&empty).abs() < f32::EPSILON);
}
#[test]
fn a_badge_fills_with_its_tone_surface_and_edges_in_its_tone() {
let p = palette(Color32::from_rgb(4, 4, 4));
assert_eq!(p.tone_surface(Tone::Neutral), p.raised);
assert_eq!(p.tone_edge(Tone::Neutral), p.border);
for (tone, surface, edge) in [
(Tone::Info, p.info_surface, p.info),
(Tone::Success, p.success_surface, p.success),
(Tone::Warning, p.warning_surface, p.warning),
(Tone::Danger, p.danger_surface, p.danger),
] {
assert_eq!(p.tone_surface(tone), surface, "{tone:?}");
assert_eq!(p.tone_edge(tone), edge, "{tone:?}");
}
}