//! Tests for [`super`]. use super::*; 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), 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), } } /// The cast is egui's own shadow type carrying the theme's tone, which is /// the whole of what this crate had to decide for it: unlike a bevel, egui /// already knows how to paint one. #[test] fn a_unit_is_drawn_only_where_the_kind_is_a_quantity() { // egui-drawing has no harness here, so what is tested is the decision // that precedes it: which fields have a unit to draw at all. The kind // half comes from the description rather than from a `matches!` in this // crate, which is the drift `FieldKind::measurable` exists to stop. 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() { // No substitution left. The page-filled well was a stand-in for a // token that did not exist yet; it exists now. 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)); } /// Sunken is its own colour, not the well's and not the page's. The two /// are authored in opposite directions and an earlier cut of the /// description conflated them. #[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() { // The cross-app bug, asserted at the renderer boundary this time. 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() { // makeover-layout 0.14.0 is what made this reachable. The answer was // already here at 0.10.0 and the question could not be asked. 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); // The shadow `frame` reaches for is the theme's tone rather than // egui's default, which is the whole reason `cast` exists. 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() { // The save-the-wrong-thing bug, asserted at the second renderer so it // is not re-found there. goingson's own numbers. let options = [ Choice::plain("1"), Choice::plain("3"), Choice::plain("7"), Choice::plain("14"), ]; assert_eq!(shown_label(&options, "10"), "10"); // And a value that does match reads as its label, not as itself. 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" ); // Answered, and it is the label that reads rather than the value. assert_eq!(chosen_text(&field, "sp404", &p), ("SP-404", p.content)); } #[test] fn a_wrong_answer_is_not_an_absent_one() { // The retention-10 bug and the ghost text meet here: a value no option // carries still reads as itself, because the field IS answered and the // answer is wrong. Only the empty value is unanswered. 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() { // The whole change is opt-in from the description. A field that says // nothing about its empty state still shows an empty box. 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() { // The distinction the kind was added for, at the renderer that has to // act on it. A well with a figure in it is not a quiet slider. 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() { // egui's `Slider` demands both ends, so inventing one would be this // renderer picking bounds the app never stated and the user then // dragging against them. A typed number takes every answer the slider // would. 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); // A bound this host cannot read is the same outcome by a different // route: the description carries bounds as text because the bound of a // date is a date. 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() { // The distinction the kind was added for, at the renderer that has to // arrange it: two wells stacked are two questions on screen, whatever // the description says. assert_eq!(control_shape(FieldKind::Interval), Control::Spanned); assert_eq!(shape_of(&Field::interval("a", "b", "A")), Control::Spanned); // Unlike a range, it owes no extent: its bounds are a rule on each end // rather than the control, so a missing one is an open end. 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() { // Which is what the shipped control did before it was described: an // unset minimum sits on the low edge and stores no filter. `DragValue` // has no empty state, and a text box instead would cost the app a // control on the way into being described. 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); // With no extent there is no edge to sit on, and a drag box has to // start somewhere. The one number this renderer invents, invented where // the description declined to say anything. 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() { // Without it a 0-to-1 threshold writes back whatever float the drag // landed on, which is the host's granularity and is what the // description says an absent step means. assert_eq!(decimals(None), None); assert_eq!(decimals(Some("1")), Some(0)); assert_eq!(decimals(Some("0.01")), Some(2)); // Trailing zeros are not precision: 0.10 is a one-decimal question. assert_eq!(decimals(Some("0.10")), Some(1)); } #[test] fn an_unavailable_option_is_drawn_muted_rather_than_dropped() { // The tone rule, at the one place it is a claim rather than a // preference: this option genuinely will not answer, so muted is the // truth. The available ones beside it keep the secondary intent. 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() { // makeover-layout 0.39.0. The radio group has room for a second line // and this control does not, so the two draw the same description // differently and neither is a preference. 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." ); // What it is, then why it cannot be picked. An option carrying both has // said two things, and the order is the one the group draws. 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 *"); // The marker is copy and the app owns it, which is why it is a knob. 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() { // What decides whether the control gets a well. A well is for what the // user looks into, and only one of these is. assert_eq!(control_shape(FieldKind::Select), Control::Chosen); // Not the wildcard. A theme picker falling to `Control::Typed` would // draw a text box over a resolved list, which is worse than the select // every app had before the member existed. 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"); } // And the two multi-line ones get a multi-line edit, which is the half // `control_shape` alone does not say: both are typed into, and only one // of the two edit shapes can hold a markdown document. assert!(FieldKind::Rich.multiline()); assert!(FieldKind::Textarea.multiline()); assert!(!FieldKind::Text.multiline()); } #[test] fn the_two_option_taking_kinds_are_drawn_differently_on_purpose() { // The description holds Select and Radio apart, and a renderer that // collapsed them would silently answer a question the app did not ask: // audiofiles' storage style is irreversible and its alternatives have // to be readable without opening anything. Asserting the two shapes // differ is asserting that distinction survives the trip. assert!(FieldKind::Select.offers_options()); assert!(FieldKind::Radio.offers_options()); assert_ne!( control_shape(FieldKind::Select), control_shape(FieldKind::Radio) ); } #[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); // The whole point of the distinction: muted is what Disabled resolves // to, so an option wearing it would claim it does not answer a press. assert_ne!(option_color("wav", "aiff", &p), p.content_muted); } /// What the accessibility tree says a screen drew, as `(role, name)` pairs. /// /// egui builds this from the same `WidgetInfo` every widget already reports, /// so it is what a screen reader would be handed rather than a second /// opinion about it. A name that arrives through `labelled_by` is resolved /// the way a client resolves it: the relation names another node, and that /// node's text is the control's accessible name. 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() }; // Two passes: egui lays out against the previous frame, so the first // sees widgets at the wrong rect. 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() { // The sharper half. With nothing attached, a box carrying a value is // announced as that value, so a rename field read out the name it was // seeded with and never said what was being asked. 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() { // A union response carries the first id, so labelling the pair outside // the arm named the lower box and left the upper one announced as // whatever number was in it. 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(), ); }); // A `SpinButton`: an interval's ends are drag values, not text boxes. 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() { // `FieldKind::labels_itself` routes past the label, and egui names a // checkbox from its own text, so there is nothing to attach and // attaching one would say the name twice. 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() { // Where the two renderers legitimately part: a webview still emits an // input because the form submits, and there is no form here. 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()); // Stating no state is the ordinary case and answers. Focus used to // be the counter-example here; it is egui's now and a description // cannot state it at all. 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() { // No panic and no write-through. A checkbox handed a string cannot be // filled, so it is drawn off and left alone. 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"); // A stored id whose theme has been deleted reads as itself rather than // as an empty box, which is the honest report on the config as it // stands. assert_eq!(themed_text(&field, "gone"), "gone"); }