Skip to main content

max / makeover-immediate

23.8 KB · 677 lines History Blame Raw
1 //! Tests for [`super`].
2
3 use super::*;
4
5 fn palette(well: Color32) -> Palette {
6 Palette {
7 page: Color32::from_rgb(1, 1, 1),
8 raised: Color32::from_rgb(2, 2, 2),
9 overlay: Color32::from_rgb(3, 3, 3),
10 well,
11 sunken: Color32::from_rgb(4, 4, 4),
12 bevel_light: Color32::WHITE,
13 bevel_dark: Color32::BLACK,
14 elevation: Color32::from_black_alpha(46),
15 content: Color32::from_rgb(5, 5, 5),
16 content_secondary: Color32::from_rgb(55, 55, 55),
17 content_muted: Color32::from_rgb(6, 6, 6),
18 action: Color32::from_rgb(7, 7, 7),
19 danger: Color32::from_rgb(8, 8, 8),
20 success: Color32::from_rgb(9, 9, 9),
21 warning: Color32::from_rgb(10, 10, 10),
22 info: Color32::from_rgb(11, 11, 11),
23 }
24 }
25
26 /// The cast is egui's own shadow type carrying the theme's tone, which is
27 /// the whole of what this crate had to decide for it: unlike a bevel, egui
28 /// already knows how to paint one.
29 #[test]
30 fn a_unit_is_drawn_only_where_the_kind_is_a_quantity() {
31 // egui-drawing has no harness here, so what is tested is the decision
32 // that precedes it: which fields have a unit to draw at all. The kind
33 // half comes from the description rather than from a `matches!` in this
34 // crate, which is the drift `FieldKind::measurable` exists to stop.
35 let ranged = makeover_layout::Field {
36 unit: Some("s"),
37 ..makeover_layout::Field::range("attack", "Attack", "0", "5")
38 };
39 assert_eq!(unit_of(&ranged), Some("s"));
40
41 let typed = makeover_layout::Field {
42 unit: Some("ms"),
43 ..makeover_layout::Field::new(makeover_layout::FieldKind::Number, "fade", "Fade")
44 };
45 assert_eq!(unit_of(&typed), Some("ms"));
46
47 let worded = makeover_layout::Field {
48 unit: Some("s"),
49 ..makeover_layout::Field::new(makeover_layout::FieldKind::Text, "name", "Name")
50 };
51 assert_eq!(unit_of(&worded), None);
52
53 let bare = makeover_layout::Field::range("attack", "Attack", "0", "5");
54 assert_eq!(unit_of(&bare), None);
55 }
56
57 #[test]
58 fn the_cast_hands_egui_the_themes_tone() {
59 let p = palette(Color32::from_rgb(9, 9, 9));
60 let cast = p.cast();
61 assert_eq!(cast.color, p.elevation);
62 assert!(cast.blur > 0, "a cast shadow is soft");
63 assert_eq!(cast.offset, [0, 2], "it falls downward and only a little");
64 }
65
66 #[test]
67 fn a_well_resolves_to_its_own_token() {
68 // No substitution left. The page-filled well was a stand-in for a
69 // token that did not exist yet; it exists now.
70 let w = Color32::from_rgb(9, 9, 9);
71 let p = palette(w);
72 assert_eq!(p.fill(Fill::Well), Some(w));
73 assert_ne!(p.fill(Fill::Well), Some(p.page));
74 }
75
76 #[test]
77 fn every_intent_is_a_plain_lookup() {
78 let p = palette(Color32::from_rgb(9, 9, 9));
79 assert_eq!(p.fill(Fill::Page), Some(p.page));
80 assert_eq!(p.fill(Fill::Raised), Some(p.raised));
81 assert_eq!(p.fill(Fill::Overlay), Some(p.overlay));
82 }
83
84 /// Sunken is its own colour, not the well's and not the page's. The two
85 /// are authored in opposite directions and an earlier cut of the
86 /// description conflated them.
87 #[test]
88 fn sunken_is_neither_the_well_nor_the_page() {
89 let p = palette(Color32::from_rgb(9, 9, 9));
90 assert_eq!(p.fill(Fill::Sunken), Some(p.sunken));
91 assert_ne!(p.fill(Fill::Sunken), p.fill(Fill::Well));
92 assert_ne!(p.fill(Fill::Sunken), p.fill(Fill::Page));
93 }
94
95 #[test]
96 fn a_raised_region_never_resolves_to_the_well_fill() {
97 // The cross-app bug, asserted at the renderer boundary this time.
98 let p = palette(Color32::from_rgb(9, 9, 9));
99 let raised = Depth::Raised.fill().and_then(|f| p.fill(f));
100 let well = Depth::Well.fill().and_then(|f| p.fill(f));
101 assert_eq!(raised, Some(p.raised));
102 assert_ne!(raised, well);
103 }
104
105 #[test]
106 fn an_overlay_is_cast_onto_the_page_and_takes_no_edge() {
107 // makeover-layout 0.14.0 is what made this reachable. The answer was
108 // already here at 0.10.0 and the question could not be asked.
109 let p = palette(Color32::from_rgb(9, 9, 9));
110 assert_eq!(
111 Depth::Overlay.fill().and_then(|f| p.fill(f)),
112 Some(p.overlay)
113 );
114 assert_eq!(Depth::Overlay.bevel(), None);
115 // The shadow `frame` reaches for is the theme's tone rather than
116 // egui's default, which is the whole reason `cast` exists.
117 assert_eq!(p.cast().color, p.elevation);
118 }
119
120 #[test]
121 fn the_lit_edge_swaps_when_a_card_is_pressed() {
122 let p = palette(Color32::from_rgb(9, 9, 9));
123 let (tl, _) = Depth::Raised.bevel().unwrap().edges();
124 let (ptl, _) = Depth::Raised.pressed().bevel().unwrap().edges();
125 assert_eq!(p.edge(tl), p.bevel_light);
126 assert_eq!(p.edge(ptl), p.bevel_dark);
127 }
128
129 #[test]
130 fn flat_asks_for_neither_fill_nor_edge() {
131 assert!(Depth::Flat.fill().is_none());
132 assert!(Depth::Flat.bevel().is_none());
133 }
134
135 #[test]
136 fn a_select_keeps_a_value_none_of_its_options_carries() {
137 // The save-the-wrong-thing bug, asserted at the second renderer so it
138 // is not re-found there. goingson's own numbers.
139 let options = [
140 Choice::plain("1"),
141 Choice::plain("3"),
142 Choice::plain("7"),
143 Choice::plain("14"),
144 ];
145 assert_eq!(shown_label(&options, "10"), "10");
146 // And a value that does match reads as its label, not as itself.
147 let spelled = [Choice::new("7", "One week")];
148 assert_eq!(shown_label(&spelled, "7"), "One week");
149 }
150
151 #[test]
152 fn an_unanswered_chooser_reads_its_ghost_text_and_reads_it_muted() {
153 let p = palette(Color32::from_rgb(9, 9, 9));
154 let options = [Choice::new("sp404", "SP-404")];
155 let field = Field {
156 placeholder: Some("Select device..."),
157 ..Field::select("device", "Conform for device", &options)
158 };
159
160 assert_eq!(
161 chosen_text(&field, "", &p),
162 ("Select device...", p.content_muted),
163 "ghost text is not an answer, so it takes the tone the typed kinds' ghost text does"
164 );
165
166 // Answered, and it is the label that reads rather than the value.
167 assert_eq!(chosen_text(&field, "sp404", &p), ("SP-404", p.content));
168 }
169
170 #[test]
171 fn a_wrong_answer_is_not_an_absent_one() {
172 // The retention-10 bug and the ghost text meet here: a value no option
173 // carries still reads as itself, because the field IS answered and the
174 // answer is wrong. Only the empty value is unanswered.
175 let p = palette(Color32::from_rgb(9, 9, 9));
176 let options = [Choice::plain("1"), Choice::plain("7")];
177 let field = Field {
178 placeholder: Some("Pick one"),
179 ..Field::select("retention", "Keep backups for", &options)
180 };
181
182 assert_eq!(chosen_text(&field, "10", &p), ("10", p.content));
183 }
184
185 #[test]
186 fn a_chooser_with_no_ghost_text_is_unchanged() {
187 // The whole change is opt-in from the description. A field that says
188 // nothing about its empty state still shows an empty box.
189 let p = palette(Color32::from_rgb(9, 9, 9));
190 let options = [Choice::plain("1")];
191 let field = Field::select("retention", "Keep backups for", &options);
192 assert_eq!(chosen_text(&field, "", &p), ("", p.content));
193 }
194
195 #[test]
196 fn a_range_is_slid_and_a_number_is_typed_into() {
197 // The distinction the kind was added for, at the renderer that has to
198 // act on it. A well with a figure in it is not a quiet slider.
199 assert_eq!(control_shape(FieldKind::Range), Control::Slid);
200 assert_eq!(control_shape(FieldKind::Number), Control::Typed);
201 }
202
203 #[test]
204 fn a_range_missing_an_end_falls_back_to_a_well() {
205 // egui's `Slider` demands both ends, so inventing one would be this
206 // renderer picking bounds the app never stated and the user then
207 // dragging against them. A typed number takes every answer the slider
208 // would.
209 let whole = Field::range("review", "Review above", "0", "1");
210 assert_eq!(shape_of(&whole), Control::Slid);
211
212 let half = Field {
213 max: Some("1"),
214 ..Field::new(FieldKind::Range, "review", "Review above")
215 };
216 assert_eq!(shape_of(&half), Control::Typed);
217 assert_eq!(extent(&half), None);
218
219 // A bound this host cannot read is the same outcome by a different
220 // route: the description carries bounds as text because the bound of a
221 // date is a date.
222 let dated = Field::range("when", "When", "2026-08-01", "2026-08-31");
223 assert_eq!(extent(&dated), None);
224 }
225
226 #[test]
227 fn an_interval_is_its_own_shape_and_not_two_numbers() {
228 // The distinction the kind was added for, at the renderer that has to
229 // arrange it: two wells stacked are two questions on screen, whatever
230 // the description says.
231 assert_eq!(control_shape(FieldKind::Interval), Control::Spanned);
232 assert_eq!(shape_of(&Field::interval("a", "b", "A")), Control::Spanned);
233
234 // Unlike a range, it owes no extent: its bounds are a rule on each end
235 // rather than the control, so a missing one is an open end.
236 let axis = Field {
237 min: Some("0"),
238 max: Some("300"),
239 ..Field::interval("bpm_min", "bpm_max", "BPM")
240 };
241 assert_eq!(shape_of(&axis), Control::Spanned);
242 }
243
244 #[test]
245 fn an_empty_end_reads_as_the_bound_it_stands_for() {
246 // Which is what the shipped control did before it was described: an
247 // unset minimum sits on the low edge and stores no filter. `DragValue`
248 // has no empty state, and a text box instead would cost the app a
249 // control on the way into being described.
250 let axis = Axis {
251 extent: Some(0.0..=300.0),
252 step: Some("1"),
253 unit: Some("BPM"),
254 };
255 assert!((axis.edge(Bound::Low) - 0.0).abs() < f64::EPSILON);
256 assert!((axis.edge(Bound::High) - 300.0).abs() < f64::EPSILON);
257
258 // With no extent there is no edge to sit on, and a drag box has to
259 // start somewhere. The one number this renderer invents, invented where
260 // the description declined to say anything.
261 let open = Axis {
262 extent: None,
263 step: None,
264 unit: None,
265 };
266 assert!((open.edge(Bound::Low) - 0.0).abs() < f64::EPSILON);
267 assert!((open.edge(Bound::High) - 0.0).abs() < f64::EPSILON);
268 }
269
270 #[test]
271 fn the_step_decides_how_a_dragged_value_is_written_back() {
272 // Without it a 0-to-1 threshold writes back whatever float the drag
273 // landed on, which is the host's granularity and is what the
274 // description says an absent step means.
275 assert_eq!(decimals(None), None);
276 assert_eq!(decimals(Some("1")), Some(0));
277 assert_eq!(decimals(Some("0.01")), Some(2));
278 // Trailing zeros are not precision: 0.10 is a one-decimal question.
279 assert_eq!(decimals(Some("0.10")), Some(1));
280 }
281
282 #[test]
283 fn an_unavailable_option_is_drawn_muted_rather_than_dropped() {
284 // The tone rule, at the one place it is a claim rather than a
285 // preference: this option genuinely will not answer, so muted is the
286 // truth. The available ones beside it keep the secondary intent.
287 let p = palette(Color32::from_rgb(9, 9, 9));
288 let options = [
289 Choice::new("chromatic", "Chromatic"),
290 Choice::new("multi", "Multi-sample").unless("Drop a second sample."),
291 ];
292 assert!(options[0].available());
293 assert!(!options[1].available());
294 assert_eq!(
295 option_color("chromatic", options[0].value, &p),
296 p.content,
297 "the chosen option is the emphasised thing"
298 );
299 assert_eq!(
300 option_color("chromatic", options[1].value, &p),
301 p.content_secondary,
302 "and `option_color` never mutes: the unavailable path is what does"
303 );
304 }
305
306 #[test]
307 fn a_closed_chooser_carries_both_of_an_options_extra_lines_in_its_row() {
308 // makeover-layout 0.39.0. The radio group has room for a second line
309 // and this control does not, so the two draw the same description
310 // differently and neither is a preference.
311 let plain = Choice::new("24", "Small Files");
312 assert_eq!(combo_row(&plain), "Small Files");
313
314 let detailed = plain.detailing("$24/mo. Fits audio, plugins, binaries.");
315 assert_eq!(
316 combo_row(&detailed),
317 "Small Files $24/mo. Fits audio, plugins, binaries."
318 );
319
320 // What it is, then why it cannot be picked. An option carrying both has
321 // said two things, and the order is the one the group draws.
322 let both = detailed.unless("Sold out.");
323 assert_eq!(
324 combo_row(&both),
325 "Small Files $24/mo. Fits audio, plugins, binaries. Sold out."
326 );
327 assert_eq!(
328 combo_row(&plain.unless("Sold out.")),
329 "Small Files Sold out.",
330 "the reason still reads the way it did before the detail existed"
331 );
332 }
333
334 #[test]
335 fn only_a_required_field_is_marked() {
336 let style = FieldStyle::default();
337 let plain = Field::new(FieldKind::Text, "title", "Title");
338 assert_eq!(label_text(&plain, &style), "Title");
339
340 let required = Field {
341 required: true,
342 ..plain
343 };
344 assert_eq!(label_text(&required, &style), "Title *");
345
346 // The marker is copy and the app owns it, which is why it is a knob.
347 let house = FieldStyle {
348 required_marker: "(required)",
349 ..style
350 };
351 assert_eq!(label_text(&required, &house), "Title (required)");
352 }
353
354 #[test]
355 fn a_select_and_a_checkbox_are_pressed_and_everything_else_is_typed_into() {
356 // What decides whether the control gets a well. A well is for what the
357 // user looks into, and only one of these is.
358 assert_eq!(control_shape(FieldKind::Select), Control::Chosen);
359 // Not the wildcard. A theme picker falling to `Control::Typed` would
360 // draw a text box over a resolved list, which is worse than the select
361 // every app had before the member existed.
362 assert_eq!(control_shape(FieldKind::Theme), Control::Themed);
363 assert_eq!(control_shape(FieldKind::Radio), Control::Listed);
364 assert_eq!(control_shape(FieldKind::Checkbox), Control::Toggled);
365 for k in [
366 FieldKind::Text,
367 FieldKind::Secret,
368 FieldKind::Number,
369 FieldKind::Email,
370 FieldKind::Url,
371 FieldKind::Tel,
372 FieldKind::Textarea,
373 FieldKind::Rich,
374 ] {
375 assert_eq!(control_shape(k), Control::Typed, "{k:?} is typed into");
376 }
377 // And the two multi-line ones get a multi-line edit, which is the half
378 // `control_shape` alone does not say: both are typed into, and only one
379 // of the two edit shapes can hold a markdown document.
380 assert!(FieldKind::Rich.multiline());
381 assert!(FieldKind::Textarea.multiline());
382 assert!(!FieldKind::Text.multiline());
383 }
384
385 #[test]
386 fn the_two_option_taking_kinds_are_drawn_differently_on_purpose() {
387 // The description holds Select and Radio apart, and a renderer that
388 // collapsed them would silently answer a question the app did not ask:
389 // audiofiles' storage style is irreversible and its alternatives have
390 // to be readable without opening anything. Asserting the two shapes
391 // differ is asserting that distinction survives the trip.
392 assert!(FieldKind::Select.offers_options());
393 assert!(FieldKind::Radio.offers_options());
394 assert_ne!(
395 control_shape(FieldKind::Select),
396 control_shape(FieldKind::Radio)
397 );
398 }
399
400 #[test]
401 fn an_unchosen_option_is_secondary_and_never_muted() {
402 let p = palette(Color32::from_rgb(4, 4, 4));
403 assert_eq!(option_color("wav", "wav", &p), p.content);
404 assert_eq!(option_color("wav", "aiff", &p), p.content_secondary);
405 // The whole point of the distinction: muted is what Disabled resolves
406 // to, so an option wearing it would claim it does not answer a press.
407 assert_ne!(option_color("wav", "aiff", &p), p.content_muted);
408 }
409
410 /// What the accessibility tree says a screen drew, as `(role, name)` pairs.
411 ///
412 /// egui builds this from the same `WidgetInfo` every widget already reports,
413 /// so it is what a screen reader would be handed rather than a second
414 /// opinion about it. A name that arrives through `labelled_by` is resolved
415 /// the way a client resolves it: the relation names another node, and that
416 /// node's text is the control's accessible name.
417 fn announced(draw: impl FnMut(&mut egui::Ui)) -> Vec<(egui::accesskit::Role, String)> {
418 let ctx = egui::Context::default();
419 ctx.enable_accesskit();
420 let mut draw = draw;
421 let input = || egui::RawInput {
422 screen_rect: Some(egui::Rect::from_min_size(
423 egui::Pos2::ZERO,
424 egui::vec2(800.0, 600.0),
425 )),
426 ..Default::default()
427 };
428 // Two passes: egui lays out against the previous frame, so the first
429 // sees widgets at the wrong rect.
430 let _ = ctx.run_ui(input(), &mut draw);
431 let out = ctx.run_ui(input(), &mut draw);
432
433 let update = out
434 .platform_output
435 .accesskit_update
436 .expect("accesskit is on, so a tree was built");
437 let by_id: std::collections::HashMap<_, _> = update.nodes.iter().cloned().collect();
438 update
439 .nodes
440 .iter()
441 .map(|(_, node)| {
442 let named = node.label().map(str::to_owned).or_else(|| {
443 node.labelled_by()
444 .iter()
445 .find_map(|id| by_id.get(id))
446 .and_then(|by| by.label().or_else(|| by.value()).map(str::to_owned))
447 });
448 (node.role(), named.unwrap_or_default())
449 })
450 .collect()
451 }
452
453 #[test]
454 fn a_fields_label_names_its_control_rather_than_sitting_beside_it() {
455 let f = Field::new(FieldKind::Text, "name", "Vault name");
456 let p = palette(Color32::from_rgb(9, 9, 9));
457 let drawn = announced(|ui| {
458 let mut text = String::new();
459 field(
460 ui,
461 &f,
462 Filling::Text(&mut text),
463 None,
464 &p,
465 &FieldStyle::default(),
466 );
467 });
468
469 let box_ = drawn
470 .iter()
471 .find(|(role, _)| *role == egui::accesskit::Role::TextInput)
472 .expect("a text field draws a text input");
473 assert_eq!(
474 box_.1, "Vault name",
475 "the box is announced by the question rather than unnamed: {drawn:?}"
476 );
477 }
478
479 #[test]
480 fn a_prefilled_box_is_still_announced_by_its_question() {
481 // The sharper half. With nothing attached, a box carrying a value is
482 // announced as that value, so a rename field read out the name it was
483 // seeded with and never said what was being asked.
484 let f = Field::new(FieldKind::Text, "name", "New name");
485 let p = palette(Color32::from_rgb(9, 9, 9));
486 let drawn = announced(|ui| {
487 let mut text = String::from("Drums");
488 field(
489 ui,
490 &f,
491 Filling::Text(&mut text),
492 None,
493 &p,
494 &FieldStyle::default(),
495 );
496 });
497
498 let box_ = drawn
499 .iter()
500 .find(|(role, _)| *role == egui::accesskit::Role::TextInput)
501 .expect("a text field draws a text input");
502 assert_eq!(box_.1, "New name", "{drawn:?}");
503 }
504
505 #[test]
506 fn both_ends_of_an_interval_are_named_by_the_one_question() {
507 // A union response carries the first id, so labelling the pair outside
508 // the arm named the lower box and left the upper one announced as
509 // whatever number was in it.
510 let f = Field::new(FieldKind::Interval, "bpm", "BPM Range");
511 let p = palette(Color32::from_rgb(9, 9, 9));
512 let drawn = announced(|ui| {
513 let mut lower = String::from("90");
514 let mut upper = String::from("300");
515 field(
516 ui,
517 &f,
518 Filling::Between {
519 lower: &mut lower,
520 upper: &mut upper,
521 },
522 None,
523 &p,
524 &FieldStyle::default(),
525 );
526 });
527
528 // A `SpinButton`: an interval's ends are drag values, not text boxes.
529 let ends: Vec<_> = drawn
530 .iter()
531 .filter(|(role, _)| *role == egui::accesskit::Role::SpinButton)
532 .collect();
533 assert_eq!(ends.len(), 2, "an interval draws two boxes: {drawn:?}");
534 for end in ends {
535 assert_eq!(end.1, "BPM Range", "{drawn:?}");
536 }
537 }
538
539 #[test]
540 fn a_checkbox_keeps_naming_itself() {
541 // `FieldKind::labels_itself` routes past the label, and egui names a
542 // checkbox from its own text, so there is nothing to attach and
543 // attaching one would say the name twice.
544 let f = Field::new(FieldKind::Checkbox, "loop", "Loop playback");
545 let p = palette(Color32::from_rgb(9, 9, 9));
546 let drawn = announced(|ui| {
547 let mut ticked = false;
548 field(
549 ui,
550 &f,
551 Filling::On(&mut ticked),
552 None,
553 &p,
554 &FieldStyle::default(),
555 );
556 });
557
558 assert!(
559 drawn
560 .iter()
561 .any(|(role, name)| *role == egui::accesskit::Role::CheckBox
562 && name == "Loop playback"),
563 "{drawn:?}"
564 );
565 }
566
567 #[test]
568 fn a_hidden_field_draws_nothing_and_answers_nothing() {
569 // Where the two renderers legitimately part: a webview still emits an
570 // input because the form submits, and there is no form here.
571 let f = Field::new(FieldKind::Hidden, "id", "Id");
572 let p = palette(Color32::from_rgb(9, 9, 9));
573 egui::__run_test_ui(|ui| {
574 let drawn = field(ui, &f, Filling::Absent, None, &p, &FieldStyle::default());
575 assert!(drawn.is_none());
576 });
577 }
578
579 #[test]
580 fn a_disabled_field_stops_answering_and_an_unstated_one_does_not() {
581 let f = Field::new(FieldKind::Text, "title", "Title");
582 let p = palette(Color32::from_rgb(9, 9, 9));
583 let style = FieldStyle::default();
584 egui::__run_test_ui(|ui| {
585 let mut text = String::from("x");
586 let disabled = field(
587 ui,
588 &f,
589 Filling::Text(&mut text),
590 Some(State::Disabled),
591 &p,
592 &style,
593 )
594 .unwrap();
595 assert!(!disabled.enabled());
596
597 // Stating no state is the ordinary case and answers. Focus used to
598 // be the counter-example here; it is egui's now and a description
599 // cannot state it at all.
600 let mut text = String::from("x");
601 let plain = field(ui, &f, Filling::Text(&mut text), None, &p, &style).unwrap();
602 assert!(plain.enabled(), "an unstated field still answers");
603 });
604 }
605
606 #[test]
607 fn a_field_described_one_way_and_filled_another_is_drawn_inert() {
608 // No panic and no write-through. A checkbox handed a string cannot be
609 // filled, so it is drawn off and left alone.
610 let f = Field::new(FieldKind::Checkbox, "done", "Done");
611 let p = palette(Color32::from_rgb(9, 9, 9));
612 let mut text = String::from("untouched");
613 egui::__run_test_ui(|ui| {
614 let drawn = field(
615 ui,
616 &f,
617 Filling::Text(&mut text),
618 None,
619 &p,
620 &FieldStyle::default(),
621 );
622 assert!(drawn.is_some());
623 });
624 assert_eq!(text, "untouched");
625 }
626
627 #[test]
628 fn the_disclosure_belongs_to_the_form_and_not_to_the_field() {
629 let fields = [
630 Field::new(FieldKind::Text, "title", "Title"),
631 Field {
632 extended: true,
633 ..Field::new(FieldKind::Text, "notes", "Notes")
634 },
635 ];
636 let style = FieldStyle::default();
637
638 let mut closed = Vec::new();
639 egui::__run_test_ui(|ui| {
640 group(ui, &fields, false, &style, |_, f| closed.push(f.name));
641 });
642 assert_eq!(closed, ["title"]);
643
644 let mut open = Vec::new();
645 egui::__run_test_ui(|ui| {
646 group(ui, &fields, true, &style, |_, f| open.push(f.name));
647 });
648 assert_eq!(open, ["title", "notes"]);
649 }
650
651 #[test]
652 fn the_default_frame_is_square_and_one_point() {
653 let d = FrameStyle::default();
654 assert_eq!(d.radius, CornerRadius::ZERO);
655 assert_eq!(d.margin, Margin::ZERO);
656 assert!((d.stroke - 1.0).abs() < f32::EPSILON);
657 }
658
659 #[test]
660 fn a_theme_picker_reads_its_chosen_theme_by_name() {
661 const THEMES: &[makeover_layout::ThemeChoice<'_>] = &[makeover_layout::ThemeChoice::new(
662 "carbonfox",
663 "Carbonfox",
664 ThemeVariant::Dark,
665 makeover_layout::Contrast::High,
666 )];
667 let field =
668 Field::theme("theme", "Theme", THEMES).following(Choice::new("system", "Follow System"));
669
670 assert_eq!(themed_text(&field, "carbonfox"), "Carbonfox");
671 assert_eq!(themed_text(&field, "system"), "Follow System");
672 // A stored id whose theme has been deleted reads as itself rather than
673 // as an empty box, which is the honest report on the config as it
674 // stands.
675 assert_eq!(themed_text(&field, "gone"), "gone");
676 }
677