Skip to main content

max / makeover-immediate

Move the crate-root test module to a sibling file lib.rs goes 2084 lines to 1409, its 34 tests moving to src/tests.rs. No public path moved, so this needs no version bump and no publish.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01EEmeiSJnmyL98QzA5Dwsvz
Author: Max Johnson <me@maxj.phd> · 2026-09-03 23:24 UTC
Signed with PGP, not checked
Commit: b5b27936f0a8956fcee5bf02b27841957bc2a182
Parent: b76390c
2 files changed, +500 insertions, -497 deletions
M src/lib.rs -497
@@ -1406,679 +1406,4 @@
1406 1406 }
1407 1407
1408 1408 #[cfg(test)]
1409 - mod tests {
1410 - use super::*;
1411 -
1412 - fn palette(well: Color32) -> Palette {
1413 - Palette {
1414 - page: Color32::from_rgb(1, 1, 1),
1415 - raised: Color32::from_rgb(2, 2, 2),
1416 - overlay: Color32::from_rgb(3, 3, 3),
1417 - well,
1418 - sunken: Color32::from_rgb(4, 4, 4),
1419 - bevel_light: Color32::WHITE,
1420 - bevel_dark: Color32::BLACK,
1421 - elevation: Color32::from_black_alpha(46),
1422 - content: Color32::from_rgb(5, 5, 5),
1423 - content_secondary: Color32::from_rgb(55, 55, 55),
1424 - content_muted: Color32::from_rgb(6, 6, 6),
1425 - action: Color32::from_rgb(7, 7, 7),
1426 - danger: Color32::from_rgb(8, 8, 8),
1427 - success: Color32::from_rgb(9, 9, 9),
1428 - warning: Color32::from_rgb(10, 10, 10),
1429 - info: Color32::from_rgb(11, 11, 11),
1430 - }
1431 - }
1432 -
1433 - /// The cast is egui's own shadow type carrying the theme's tone, which is
1434 - /// the whole of what this crate had to decide for it: unlike a bevel, egui
1435 - /// already knows how to paint one.
1436 - #[test]
1437 - fn a_unit_is_drawn_only_where_the_kind_is_a_quantity() {
1438 - // egui-drawing has no harness here, so what is tested is the decision
1439 - // that precedes it: which fields have a unit to draw at all. The kind
1440 - // half comes from the description rather than from a `matches!` in this
1441 - // crate, which is the drift `FieldKind::measurable` exists to stop.
1442 - let ranged = makeover_layout::Field {
1443 - unit: Some("s"),
1444 - ..makeover_layout::Field::range("attack", "Attack", "0", "5")
1445 - };
1446 - assert_eq!(unit_of(&ranged), Some("s"));
1447 -
1448 - let typed = makeover_layout::Field {
1449 - unit: Some("ms"),
1450 - ..makeover_layout::Field::new(makeover_layout::FieldKind::Number, "fade", "Fade")
1451 - };
1452 - assert_eq!(unit_of(&typed), Some("ms"));
1453 -
1454 - let worded = makeover_layout::Field {
1455 - unit: Some("s"),
1456 - ..makeover_layout::Field::new(makeover_layout::FieldKind::Text, "name", "Name")
1457 - };
1458 - assert_eq!(unit_of(&worded), None);
1459 -
1460 - let bare = makeover_layout::Field::range("attack", "Attack", "0", "5");
1461 - assert_eq!(unit_of(&bare), None);
1462 - }
1463 -
1464 - #[test]
1465 - fn the_cast_hands_egui_the_themes_tone() {
1466 - let p = palette(Color32::from_rgb(9, 9, 9));
1467 - let cast = p.cast();
1468 - assert_eq!(cast.color, p.elevation);
1469 - assert!(cast.blur > 0, "a cast shadow is soft");
1470 - assert_eq!(cast.offset, [0, 2], "it falls downward and only a little");
1471 - }
1472 -
1473 - #[test]
1474 - fn a_well_resolves_to_its_own_token() {
1475 - // No substitution left. The page-filled well was a stand-in for a
1476 - // token that did not exist yet; it exists now.
1477 - let w = Color32::from_rgb(9, 9, 9);
1478 - let p = palette(w);
1479 - assert_eq!(p.fill(Fill::Well), Some(w));
1480 - assert_ne!(p.fill(Fill::Well), Some(p.page));
1481 - }
1482 -
1483 - #[test]
1484 - fn every_intent_is_a_plain_lookup() {
1485 - let p = palette(Color32::from_rgb(9, 9, 9));
1486 - assert_eq!(p.fill(Fill::Page), Some(p.page));
1487 - assert_eq!(p.fill(Fill::Raised), Some(p.raised));
1488 - assert_eq!(p.fill(Fill::Overlay), Some(p.overlay));
1489 - }
1490 -
1491 - /// Sunken is its own colour, not the well's and not the page's. The two
1492 - /// are authored in opposite directions and an earlier cut of the
1493 - /// description conflated them.
1494 - #[test]
1495 - fn sunken_is_neither_the_well_nor_the_page() {
1496 - let p = palette(Color32::from_rgb(9, 9, 9));
1497 - assert_eq!(p.fill(Fill::Sunken), Some(p.sunken));
1498 - assert_ne!(p.fill(Fill::Sunken), p.fill(Fill::Well));
1499 - assert_ne!(p.fill(Fill::Sunken), p.fill(Fill::Page));
1500 - }
1501 -
1502 - #[test]
1503 - fn a_raised_region_never_resolves_to_the_well_fill() {
1504 - // The cross-app bug, asserted at the renderer boundary this time.
1505 - let p = palette(Color32::from_rgb(9, 9, 9));
1506 - let raised = Depth::Raised.fill().and_then(|f| p.fill(f));
1507 - let well = Depth::Well.fill().and_then(|f| p.fill(f));
1508 - assert_eq!(raised, Some(p.raised));
1509 - assert_ne!(raised, well);
1510 - }
1511 -
1512 - #[test]
1513 - fn an_overlay_is_cast_onto_the_page_and_takes_no_edge() {
1514 - // makeover-layout 0.14.0 is what made this reachable. The answer was
1515 - // already here at 0.10.0 and the question could not be asked.
1516 - let p = palette(Color32::from_rgb(9, 9, 9));
1517 - assert_eq!(
1518 - Depth::Overlay.fill().and_then(|f| p.fill(f)),
1519 - Some(p.overlay)
1520 - );
1521 - assert_eq!(Depth::Overlay.bevel(), None);
1522 - // The shadow `frame` reaches for is the theme's tone rather than
1523 - // egui's default, which is the whole reason `cast` exists.
1524 - assert_eq!(p.cast().color, p.elevation);
1525 - }
1526 -
1527 - #[test]
1528 - fn the_lit_edge_swaps_when_a_card_is_pressed() {
1529 - let p = palette(Color32::from_rgb(9, 9, 9));
1530 - let (tl, _) = Depth::Raised.bevel().unwrap().edges();
1531 - let (ptl, _) = Depth::Raised.pressed().bevel().unwrap().edges();
1532 - assert_eq!(p.edge(tl), p.bevel_light);
1533 - assert_eq!(p.edge(ptl), p.bevel_dark);
1534 - }
1535 -
1536 - #[test]
1537 - fn flat_asks_for_neither_fill_nor_edge() {
1538 - assert!(Depth::Flat.fill().is_none());
1539 - assert!(Depth::Flat.bevel().is_none());
1540 - }
1541 -
1542 - #[test]
1543 - fn a_select_keeps_a_value_none_of_its_options_carries() {
1544 - // The save-the-wrong-thing bug, asserted at the second renderer so it
1545 - // is not re-found there. goingson's own numbers.
1546 - let options = [
1547 - Choice::plain("1"),
1548 - Choice::plain("3"),
1549 - Choice::plain("7"),
1550 - Choice::plain("14"),
1551 - ];
1552 - assert_eq!(shown_label(&options, "10"), "10");
1553 - // And a value that does match reads as its label, not as itself.
1554 - let spelled = [Choice::new("7", "One week")];
1555 - assert_eq!(shown_label(&spelled, "7"), "One week");
1556 - }
1557 -
1558 - #[test]
1559 - fn an_unanswered_chooser_reads_its_ghost_text_and_reads_it_muted() {
1560 - let p = palette(Color32::from_rgb(9, 9, 9));
1561 - let options = [Choice::new("sp404", "SP-404")];
1562 - let field = Field {
1563 - placeholder: Some("Select device..."),
1564 - ..Field::select("device", "Conform for device", &options)
1565 - };
1566 -
1567 - assert_eq!(
1568 - chosen_text(&field, "", &p),
1569 - ("Select device...", p.content_muted),
1570 - "ghost text is not an answer, so it takes the tone the typed kinds' ghost text does"
1571 - );
1572 -
1573 - // Answered, and it is the label that reads rather than the value.
1574 - assert_eq!(chosen_text(&field, "sp404", &p), ("SP-404", p.content));
1575 - }
1576 -
1577 - #[test]
1578 - fn a_wrong_answer_is_not_an_absent_one() {
1579 - // The retention-10 bug and the ghost text meet here: a value no option
1580 - // carries still reads as itself, because the field IS answered and the
1581 - // answer is wrong. Only the empty value is unanswered.
1582 - let p = palette(Color32::from_rgb(9, 9, 9));
1583 - let options = [Choice::plain("1"), Choice::plain("7")];
1584 - let field = Field {
1585 - placeholder: Some("Pick one"),
1586 - ..Field::select("retention", "Keep backups for", &options)
1587 - };
1588 -
1589 - assert_eq!(chosen_text(&field, "10", &p), ("10", p.content));
1590 - }
1591 -
1592 - #[test]
1593 - fn a_chooser_with_no_ghost_text_is_unchanged() {
1594 - // The whole change is opt-in from the description. A field that says
1595 - // nothing about its empty state still shows an empty box.
1596 - let p = palette(Color32::from_rgb(9, 9, 9));
1597 - let options = [Choice::plain("1")];
1598 - let field = Field::select("retention", "Keep backups for", &options);
1599 - assert_eq!(chosen_text(&field, "", &p), ("", p.content));
1600 - }
1601 -
1602 - #[test]
1603 - fn a_range_is_slid_and_a_number_is_typed_into() {
1604 - // The distinction the kind was added for, at the renderer that has to
1605 - // act on it. A well with a figure in it is not a quiet slider.
1606 - assert_eq!(control_shape(FieldKind::Range), Control::Slid);
1607 - assert_eq!(control_shape(FieldKind::Number), Control::Typed);
1608 - }
1609 -
1610 - #[test]
1611 - fn a_range_missing_an_end_falls_back_to_a_well() {
1612 - // egui's `Slider` demands both ends, so inventing one would be this
1613 - // renderer picking bounds the app never stated and the user then
1614 - // dragging against them. A typed number takes every answer the slider
1615 - // would.
1616 - let whole = Field::range("review", "Review above", "0", "1");
1617 - assert_eq!(shape_of(&whole), Control::Slid);
1618 -
1619 - let half = Field {
1620 - max: Some("1"),
1621 - ..Field::new(FieldKind::Range, "review", "Review above")
1622 - };
1623 - assert_eq!(shape_of(&half), Control::Typed);
1624 - assert_eq!(extent(&half), None);
1625 -
1626 - // A bound this host cannot read is the same outcome by a different
1627 - // route: the description carries bounds as text because the bound of a
1628 - // date is a date.
1629 - let dated = Field::range("when", "When", "2026-08-01", "2026-08-31");
1630 - assert_eq!(extent(&dated), None);
1631 - }
1632 -
1633 - #[test]
1634 - fn an_interval_is_its_own_shape_and_not_two_numbers() {
1635 - // The distinction the kind was added for, at the renderer that has to
1636 - // arrange it: two wells stacked are two questions on screen, whatever
1637 - // the description says.
1638 - assert_eq!(control_shape(FieldKind::Interval), Control::Spanned);
1639 - assert_eq!(shape_of(&Field::interval("a", "b", "A")), Control::Spanned);
1640 -
1641 - // Unlike a range, it owes no extent: its bounds are a rule on each end
1642 - // rather than the control, so a missing one is an open end.
1643 - let axis = Field {
1644 - min: Some("0"),
1645 - max: Some("300"),
1646 - ..Field::interval("bpm_min", "bpm_max", "BPM")
1647 - };
1648 - assert_eq!(shape_of(&axis), Control::Spanned);
1649 - }
1650 -
1651 - #[test]
1652 - fn an_empty_end_reads_as_the_bound_it_stands_for() {
1653 - // Which is what the shipped control did before it was described: an
1654 - // unset minimum sits on the low edge and stores no filter. `DragValue`
1655 - // has no empty state, and a text box instead would cost the app a
1656 - // control on the way into being described.
1657 - let axis = Axis {
1658 - extent: Some(0.0..=300.0),
1659 - step: Some("1"),
1660 - unit: Some("BPM"),
1661 - };
1662 - assert!((axis.edge(Bound::Low) - 0.0).abs() < f64::EPSILON);
1663 - assert!((axis.edge(Bound::High) - 300.0).abs() < f64::EPSILON);
1664 -
1665 - // With no extent there is no edge to sit on, and a drag box has to
1666 - // start somewhere. The one number this renderer invents, invented where
1667 - // the description declined to say anything.
1668 - let open = Axis {
1669 - extent: None,
1670 - step: None,
1671 - unit: None,
1672 - };
1673 - assert!((open.edge(Bound::Low) - 0.0).abs() < f64::EPSILON);
1674 - assert!((open.edge(Bound::High) - 0.0).abs() < f64::EPSILON);
1675 - }
1676 -
1677 - #[test]
1678 - fn the_step_decides_how_a_dragged_value_is_written_back() {
1679 - // Without it a 0-to-1 threshold writes back whatever float the drag
1680 - // landed on, which is the host's granularity and is what the
1681 - // description says an absent step means.
1682 - assert_eq!(decimals(None), None);
1683 - assert_eq!(decimals(Some("1")), Some(0));
1684 - assert_eq!(decimals(Some("0.01")), Some(2));
1685 - // Trailing zeros are not precision: 0.10 is a one-decimal question.
1686 - assert_eq!(decimals(Some("0.10")), Some(1));
1687 - }
1688 -
1689 - #[test]
1690 - fn an_unavailable_option_is_drawn_muted_rather_than_dropped() {
1691 - // The tone rule, at the one place it is a claim rather than a
1692 - // preference: this option genuinely will not answer, so muted is the
1693 - // truth. The available ones beside it keep the secondary intent.
1694 - let p = palette(Color32::from_rgb(9, 9, 9));
1695 - let options = [
1696 - Choice::new("chromatic", "Chromatic"),
1697 - Choice::new("multi", "Multi-sample").unless("Drop a second sample."),
1698 - ];
1699 - assert!(options[0].available());
1700 - assert!(!options[1].available());
1701 - assert_eq!(
1702 - option_color("chromatic", options[0].value, &p),
1703 - p.content,
1704 - "the chosen option is the emphasised thing"
1705 - );
1706 - assert_eq!(
1707 - option_color("chromatic", options[1].value, &p),
1708 - p.content_secondary,
1709 - "and `option_color` never mutes: the unavailable path is what does"
1710 - );
1711 - }
1712 -
1713 - #[test]
1714 - fn a_closed_chooser_carries_both_of_an_options_extra_lines_in_its_row() {
1715 - // makeover-layout 0.39.0. The radio group has room for a second line
1716 - // and this control does not, so the two draw the same description
1717 - // differently and neither is a preference.
1718 - let plain = Choice::new("24", "Small Files");
1719 - assert_eq!(combo_row(&plain), "Small Files");
1720 -
1721 - let detailed = plain.detailing("$24/mo. Fits audio, plugins, binaries.");
1722 - assert_eq!(
1723 - combo_row(&detailed),
1724 - "Small Files $24/mo. Fits audio, plugins, binaries."
1725 - );
1726 -
1727 - // What it is, then why it cannot be picked. An option carrying both has
1728 - // said two things, and the order is the one the group draws.
1729 - let both = detailed.unless("Sold out.");
1730 - assert_eq!(
1731 - combo_row(&both),
1732 - "Small Files $24/mo. Fits audio, plugins, binaries. Sold out."
1733 - );
1734 - assert_eq!(
1735 - combo_row(&plain.unless("Sold out.")),
1736 - "Small Files Sold out.",
1737 - "the reason still reads the way it did before the detail existed"
1738 - );
1739 - }
1740 -
1741 - #[test]
1742 - fn only_a_required_field_is_marked() {
1743 - let style = FieldStyle::default();
1744 - let plain = Field::new(FieldKind::Text, "title", "Title");
1745 - assert_eq!(label_text(&plain, &style), "Title");
1746 -
1747 - let required = Field {
1748 - required: true,
1749 - ..plain
1750 - };
1751 - assert_eq!(label_text(&required, &style), "Title *");
1752 -
1753 - // The marker is copy and the app owns it, which is why it is a knob.
1754 - let house = FieldStyle {
1755 - required_marker: "(required)",
1756 - ..style
1757 - };
1758 - assert_eq!(label_text(&required, &house), "Title (required)");
1759 - }
1760 -
1761 - #[test]
1762 - fn a_select_and_a_checkbox_are_pressed_and_everything_else_is_typed_into() {
1763 - // What decides whether the control gets a well. A well is for what the
1764 - // user looks into, and only one of these is.
1765 - assert_eq!(control_shape(FieldKind::Select), Control::Chosen);
1766 - // Not the wildcard. A theme picker falling to `Control::Typed` would
1767 - // draw a text box over a resolved list, which is worse than the select
1768 - // every app had before the member existed.
1769 - assert_eq!(control_shape(FieldKind::Theme), Control::Themed);
1770 - assert_eq!(control_shape(FieldKind::Radio), Control::Listed);
1771 - assert_eq!(control_shape(FieldKind::Checkbox), Control::Toggled);
1772 - for k in [
1773 - FieldKind::Text,
1774 - FieldKind::Secret,
1775 - FieldKind::Number,
1776 - FieldKind::Email,
1777 - FieldKind::Url,
1778 - FieldKind::Tel,
1779 - FieldKind::Textarea,
1780 - FieldKind::Rich,
1781 - ] {
1782 - assert_eq!(control_shape(k), Control::Typed, "{k:?} is typed into");
1783 - }
1784 - // And the two multi-line ones get a multi-line edit, which is the half
1785 - // `control_shape` alone does not say: both are typed into, and only one
1786 - // of the two edit shapes can hold a markdown document.
1787 - assert!(FieldKind::Rich.multiline());
1788 - assert!(FieldKind::Textarea.multiline());
1789 - assert!(!FieldKind::Text.multiline());
1790 - }
1791 -
1792 - #[test]
1793 - fn the_two_option_taking_kinds_are_drawn_differently_on_purpose() {
1794 - // The description holds Select and Radio apart, and a renderer that
1795 - // collapsed them would silently answer a question the app did not ask:
1796 - // audiofiles' storage style is irreversible and its alternatives have
1797 - // to be readable without opening anything. Asserting the two shapes
1798 - // differ is asserting that distinction survives the trip.
1799 - assert!(FieldKind::Select.offers_options());
1800 - assert!(FieldKind::Radio.offers_options());
1801 - assert_ne!(
1802 - control_shape(FieldKind::Select),
1803 - control_shape(FieldKind::Radio)
1804 - );
1805 - }
1806 -
1807 - #[test]
1808 - fn an_unchosen_option_is_secondary_and_never_muted() {
1809 - let p = palette(Color32::from_rgb(4, 4, 4));
1810 - assert_eq!(option_color("wav", "wav", &p), p.content);
1811 - assert_eq!(option_color("wav", "aiff", &p), p.content_secondary);
1812 - // The whole point of the distinction: muted is what Disabled resolves
1813 - // to, so an option wearing it would claim it does not answer a press.
1814 - assert_ne!(option_color("wav", "aiff", &p), p.content_muted);
1815 - }
1816 -
1817 - /// What the accessibility tree says a screen drew, as `(role, name)` pairs.
1818 - ///
1819 - /// egui builds this from the same `WidgetInfo` every widget already reports,
1820 - /// so it is what a screen reader would be handed rather than a second
1821 - /// opinion about it. A name that arrives through `labelled_by` is resolved
1822 - /// the way a client resolves it: the relation names another node, and that
1823 - /// node's text is the control's accessible name.
1824 - fn announced(draw: impl FnMut(&mut egui::Ui)) -> Vec<(egui::accesskit::Role, String)> {
1825 - let ctx = egui::Context::default();
1826 - ctx.enable_accesskit();
1827 - let mut draw = draw;
1828 - let input = || egui::RawInput {
1829 - screen_rect: Some(egui::Rect::from_min_size(
1830 - egui::Pos2::ZERO,
1831 - egui::vec2(800.0, 600.0),
1832 - )),
1833 - ..Default::default()
1834 - };
1835 - // Two passes: egui lays out against the previous frame, so the first
1836 - // sees widgets at the wrong rect.
1837 - let _ = ctx.run_ui(input(), &mut draw);
1838 - let out = ctx.run_ui(input(), &mut draw);
1839 -
1840 - let update = out
1841 - .platform_output
1842 - .accesskit_update
1843 - .expect("accesskit is on, so a tree was built");
1844 - let by_id: std::collections::HashMap<_, _> = update.nodes.iter().cloned().collect();
1845 - update
1846 - .nodes
1847 - .iter()
1848 - .map(|(_, node)| {
1849 - let named = node.label().map(str::to_owned).or_else(|| {
1850 - node.labelled_by()
1851 - .iter()
1852 - .find_map(|id| by_id.get(id))
1853 - .and_then(|by| by.label().or_else(|| by.value()).map(str::to_owned))
1854 - });
1855 - (node.role(), named.unwrap_or_default())
1856 - })
1857 - .collect()
1858 - }
1859 -
1860 - #[test]
1861 - fn a_fields_label_names_its_control_rather_than_sitting_beside_it() {
1862 - let f = Field::new(FieldKind::Text, "name", "Vault name");
1863 - let p = palette(Color32::from_rgb(9, 9, 9));
1864 - let drawn = announced(|ui| {
1865 - let mut text = String::new();
1866 - field(
1867 - ui,
1868 - &f,
1869 - Filling::Text(&mut text),
1870 - None,
1871 - &p,
1872 - &FieldStyle::default(),
1873 - );
1874 - });
1875 -
1876 - let box_ = drawn
1877 - .iter()
1878 - .find(|(role, _)| *role == egui::accesskit::Role::TextInput)
1879 - .expect("a text field draws a text input");
1880 - assert_eq!(
1881 - box_.1, "Vault name",
1882 - "the box is announced by the question rather than unnamed: {drawn:?}"
1883 - );
1884 - }
1885 -
1886 - #[test]
1887 - fn a_prefilled_box_is_still_announced_by_its_question() {
1888 - // The sharper half. With nothing attached, a box carrying a value is
1889 - // announced as that value, so a rename field read out the name it was
1890 - // seeded with and never said what was being asked.
1891 - let f = Field::new(FieldKind::Text, "name", "New name");
1892 - let p = palette(Color32::from_rgb(9, 9, 9));
1893 - let drawn = announced(|ui| {
1894 - let mut text = String::from("Drums");
1895 - field(
1896 - ui,
1897 - &f,
1898 - Filling::Text(&mut text),
1899 - None,
1900 - &p,
1901 - &FieldStyle::default(),
1902 - );
1903 - });
1904 -
1905 - let box_ = drawn
Lines truncated
A src/tests.rs +500
@@ -1,0 +1,676 @@
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)
Lines truncated