Skip to main content

max / quasi

Assert that no renderer's answer depends on how it got here One test per renderer for "Any width, one answer", the rule `makeover-layout` 0.27.4 states beside "First paint is final paint": the same description at the same width is the same output, whatever widths came before it. Each reuses one renderer instance across the whole sequence, which is the half that matters. A fresh renderer per frame could not fail these however much geometry it kept, so the thing being caught is a cutoff or a measure cached between frames. The webview's is a different assertion because it keeps the property a different way: it is never told the width at all, so the test is that the markup carries no absolute length and that a reused renderer answers a fresh one. The terminal and egui both draw at a width, so theirs is a sequence of widths returning to where it started. A regression guard rather than a bug-finder: nothing in the three renderers holds geometry across frames today. What it buys is that the next one cannot quietly start. Also carries a forward fix from a parallel session: makeover-immediate 0.25.0 to 0.26.0, whose Palette grows a content_secondary field, and the one line in the test palette that answers it.
Author: Max Johnson <me@maxj.phd> · 2026-08-16 21:43 UTC
Signed with PGP, not checked
Commit: 5e9badc3ef5e15363beb99308484756b27cf7c85
Parent: 3626a3b
5 files changed, +264 insertions, -11 deletions
M Cargo.lock +10 -10
@@ -2348,9 +2348,9 @@
2348 2348
2349 2349 [[package]]
2350 2350 name = "makeover-immediate"
2351 - version = "0.25.0"
2351 + version = "0.26.0"
2352 2352 source = "registry+https://github.com/rust-lang/crates.io-index"
2353 - checksum = "c78ddbf237c617715e8ed0c174a46f52d7135a388a0f786a164acc2bca8cb97f"
2353 + checksum = "e9946bbd05379c4aa025389d129c44440006c4aad2ec039a9e92ef7061b95191"
2354 2354 dependencies = [
2355 2355 "egui",
2356 2356 "egui_extras",
@@ -5624,14 +5624,6 @@
5624 5624 source = "registry+https://github.com/rust-lang/crates.io-index"
5625 5625 checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
5626 5626
5627 - [[patch.unused]]
5628 - name = "synckit-client"
5629 - version = "0.8.0"
5630 -
5631 - [[patch.unused]]
5632 - name = "synckit-config"
5633 - version = "0.2.0"
5634 -
5635 5627 [[patch.unused]]
5636 5628 name = "kberg"
5637 5629 version = "0.1.0"
@@ -5647,3 +5639,11 @@
5647 5639 [[patch.unused]]
5648 5640 name = "tagtree"
5649 5641 version = "0.4.0"
5642 +
5643 + [[patch.unused]]
5644 + name = "synckit-client"
5645 + version = "0.8.0"
5646 +
5647 + [[patch.unused]]
5648 + name = "synckit-config"
5649 + version = "0.2.0"
@@ -17,7 +17,7 @@
17 17 # widget here that is not a container comes from it: a screen walk that painted
18 18 # its own meter would be the divergence the suite exists to end, one copy per
19 19 # renderer instead of one copy per app.
20 - makeover-immediate = "0.25.0"
20 + makeover-immediate = "0.26.0"
21 21 egui = { version = "0.35", default-features = false }
22 22 # Markdown to text for `Node::Rich`. The same crate the webview renderer takes,
23 23 # reading the same source: what differs is that it can emit markup and this
@@ -25,6 +25,7 @@
25 25 bevel_dark: Color32::BLACK,
26 26 elevation: Color32::from_black_alpha(46),
27 27 content: Color32::from_rgb(6, 6, 6),
28 + content_secondary: Color32::from_rgb(66, 66, 66),
28 29 content_muted: Color32::from_rgb(7, 7, 7),
29 30 action: Color32::from_rgb(8, 8, 8),
30 31 danger: Color32::from_rgb(9, 9, 9),
@@ -796,3 +797,105 @@
796 797 }]);
797 798 draw(&screen, &mut view);
798 799 }
800 +
801 + /// One of every shape that could plausibly cache a size.
802 + ///
803 + /// A field at each [`layout::Width`], a table with mixed
804 + /// [`layout::Priority`], a list that says how much more there is, and a nested
805 + /// region. Written out here rather than shared with the other two renderers'
806 + /// copies: the fixture is a few lines, and sharing it would mean a new public
807 + /// surface on a crate for the sake of a test.
808 + fn every_shape_that_could_cache() -> Screen {
809 + Screen::sidebar_content("Any width").with(
810 + Slot::new("main", RegionKind::Pane)
811 + .with(Node::Field(Box::new(Field::new(
812 + layout::FieldKind::Text,
813 + "wide",
814 + "Wide",
815 + ))))
816 + .with(Node::Field(Box::new(
817 + Field::new(layout::FieldKind::Text, "tight", "Tight").width(layout::Width::Content),
818 + )))
819 + .with(Node::Field(Box::new(
820 + Field::new(layout::FieldKind::Text, "held", "Held").width(layout::Width::Fixed),
821 + )))
822 + .with(Node::Table {
823 + columns: vec![
824 + quasi_router::Column::new("Name").priority(layout::Priority::Essential),
825 + quasi_router::Column::new("Kind").priority(layout::Priority::Secondary),
826 + quasi_router::Column::new("Added").priority(layout::Priority::Optional),
827 + ],
828 + rows: vec![quasi_router::Cells::new([
829 + "kick.wav",
830 + "sample",
831 + "2026-08-12",
832 + ])],
833 + more: Some(quasi_router::Rest::more(1, Action::get("/samples?from=1"))),
834 + })
835 + .with(Node::List {
836 + rows: vec![quasi_router::Row::new("one"), quasi_router::Row::new("two")],
837 + more: Some(quasi_router::Rest::more(2, Action::get("/rows?from=2"))),
838 + })
839 + .with(Node::Region(
840 + Slot::group("nested").with(Node::text("inside")),
841 + )),
842 + )
843 + }
844 +
845 + /// Everything the renderer put on the screen at this width, as text.
846 + ///
847 + /// Through a real `Context` rather than `__run_test_ui`, because the width is
848 + /// the whole subject here and the test helper picks its own. The shapes are
849 + /// compared by their debug form: what is being asserted is that two frames are
850 + /// the same picture, and equality of the primitives is the strongest available
851 + /// statement of that.
852 + fn painted(
853 + ctx: &egui::Context,
854 + immediate: &Immediate,
855 + screen: &Screen,
856 + view: &mut View,
857 + width: f32,
858 + ) -> String {
859 + let input = egui::RawInput {
860 + screen_rect: Some(egui::Rect::from_min_size(
861 + egui::Pos2::ZERO,
862 + egui::vec2(width, 900.0),
863 + )),
864 + ..Default::default()
865 + };
866 + let output = ctx.run_ui(input, |ctx| {
867 + egui::CentralPanel::default().show(ctx, |ui| {
868 + immediate.screen(ui, screen, view);
869 + });
870 + });
871 + format!("{:?}", output.shapes)
872 + }
873 +
874 + #[test]
875 + fn a_frame_is_the_same_picture_however_the_window_got_here() {
876 + // "Any width, one answer", `makeover-layout` 0.27.4. The same description
877 + // at the same width is the same frame, whatever widths came before it.
878 + //
879 + // The renderer, the context and the view are made once and reused across
880 + // the sequence, which is the half that matters: a fresh `Immediate` per
881 + // frame could not fail this test however much geometry it kept. egui makes
882 + // the property easy to lose rather than easy to keep -- an immediate-mode
883 + // library hands you `ui.available_width()` every frame and a memory store
884 + // to put the answer in -- so this is the renderer where the guard earns
885 + // its place.
886 + let screen = every_shape_that_could_cache();
887 + let ctx = egui::Context::default();
888 + let immediate = renderer();
889 + let mut view = View::new();
890 +
891 + let cold = painted(&ctx, &immediate, &screen, &mut view, 400.0);
892 +
893 + for width in [1200.0, 320.0, 900.0, 200.0] {
894 + let _ = painted(&ctx, &immediate, &screen, &mut view, width);
895 + }
896 + assert_eq!(painted(&ctx, &immediate, &screen, &mut view, 400.0), cold);
897 +
898 + // And the fixture is one the width actually moves, or the assertion above
899 + // would be true of a blank frame.
900 + assert_ne!(painted(&ctx, &immediate, &screen, &mut view, 1200.0), cold);
901 + }
@@ -1667,3 +1667,77 @@
1667 1667 assert!(out.contains("first") && out.contains("second"), "{out}");
1668 1668 assert!(!out.contains("< Prev >"), "{out}");
1669 1669 }
1670 +
1671 + /// One of every shape that could plausibly cache a size.
1672 + ///
1673 + /// A field at each [`layout::Width`], a table with mixed
1674 + /// [`layout::Priority`], a list that says how much more there is, and a nested
1675 + /// region. Written out here rather than shared with the other two renderers'
1676 + /// copies of it: the fixture is a few lines and sharing it would mean a new
1677 + /// public surface on a crate for the sake of a test.
1678 + fn every_shape_that_could_cache() -> Screen {
1679 + Screen::sidebar_content("Any width").with(
1680 + Slot::new("main", RegionKind::Pane)
1681 + .with(Node::Field(Box::new(Field::new(
1682 + layout::FieldKind::Text,
1683 + "wide",
1684 + "Wide",
1685 + ))))
1686 + .with(Node::Field(Box::new(
1687 + Field::new(layout::FieldKind::Text, "tight", "Tight").width(layout::Width::Content),
1688 + )))
1689 + .with(Node::Field(Box::new(
1690 + Field::new(layout::FieldKind::Text, "held", "Held").width(layout::Width::Fixed),
1691 + )))
1692 + .with(Node::Table {
1693 + columns: vec![
1694 + Column::new("Name").priority(layout::Priority::Essential),
1695 + Column::new("Kind").priority(layout::Priority::Secondary),
1696 + Column::new("Added").priority(layout::Priority::Optional),
1697 + ],
1698 + rows: vec![Cells::new(["kick.wav", "sample", "2026-08-12"])],
1699 + more: Some(Rest::more(1, Action::get("/samples?from=1"))),
1700 + })
1701 + .with(Node::List {
1702 + rows: vec![Row::new("one"), Row::new("two")],
1703 + more: Some(Rest::more(2, Action::get("/rows?from=2"))),
1704 + })
1705 + .with(Node::Region(
1706 + Slot::group("nested").with(Node::text("inside")),
1707 + )),
1708 + )
1709 + }
1710 +
1711 + #[test]
1712 + fn a_narrow_terminal_draws_the_same_thing_however_it_got_narrow() {
1713 + // "Any width, one answer", `makeover-layout` 0.27.4. The same description
1714 + // at the same width is the same picture, whatever widths came before it.
1715 + //
1716 + // The renderer and the view are made once and reused across the sequence,
1717 + // which is the half that matters: a fresh `Tui` per draw could not fail
1718 + // this test however much geometry it kept. What it catches is a renderer
1719 + // that remembers -- a cutoff cached on the first pass, a column measure
1720 + // stored beside the theme -- and that is exactly the shape of the bug that
1721 + // makes an ordinary page's sidebar depend on the order you dragged the
1722 + // window.
1723 + let screen = every_shape_that_could_cache();
1724 + let tui = tui();
1725 + let view = View::new();
1726 +
1727 + let draw = |width: u16| {
1728 + let area = Rect::new(0, 0, width, 24);
1729 + let mut buf = Buffer::empty(area);
1730 + tui.screen(&screen, &view, area, &mut buf);
1731 + rows(&buf)
1732 + };
1733 +
1734 + let cold = draw(40);
1735 + for width in [120, 200, 40, 12, 400] {
1736 + let _ = draw(width);
1737 + }
1738 + assert_eq!(draw(40), cold);
1739 +
1740 + // And the fixture is one that narrowing actually bites, or the assertion
1741 + // above would be true of an empty screen.
1742 + assert_ne!(draw(120), cold);
1743 + }
@@ -3304,3 +3304,79 @@
3304 3304 assert_eq!(both.matches("field-writes").count(), 1);
3305 3305 assert!(both.contains("data-width=\"content\""));
3306 3306 }
3307 +
3308 + /// One of every shape that could plausibly cache a size.
3309 + ///
3310 + /// A field at each [`layout::Width`], a table with mixed
3311 + /// [`layout::Priority`], a list that says how much more there is, and a nested
3312 + /// region. Shared by the three renderers' path-independence tests, written out
3313 + /// in each rather than lifted into a crate they would all have to depend on:
3314 + /// the fixture is six lines and a shared one would be a new public surface.
3315 + fn every_shape_that_could_cache() -> Screen {
3316 + Screen::sidebar_content("Any width").with(
3317 + Slot::new("main", RegionKind::Pane)
3318 + .with(Node::Field(Box::new(Field::new(
3319 + layout::FieldKind::Text,
3320 + "wide",
3321 + "Wide",
3322 + ))))
3323 + .with(Node::Field(Box::new(
3324 + Field::new(layout::FieldKind::Text, "tight", "Tight").width(layout::Width::Content),
3325 + )))
3326 + .with(Node::Field(Box::new(
3327 + Field::new(layout::FieldKind::Text, "held", "Held").width(layout::Width::Fixed),
3328 + )))
3329 + .with(Node::Table {
3330 + columns: vec![
3331 + Column::new("Name").priority(layout::Priority::Essential),
3332 + Column::new("Kind").priority(layout::Priority::Secondary),
3333 + Column::new("Added").priority(layout::Priority::Optional),
3334 + ],
3335 + rows: vec![Cells::new(["kick.wav", "sample", "2026-08-12"])],
3336 + more: Some(Rest::more(1, Action::get("/samples?from=1"))),
3337 + })
3338 + .with(Node::List {
3339 + rows: vec![Row::new("one"), Row::new("two")],
3340 + more: Some(Rest::more(2, Action::get("/rows?from=2"))),
3341 + })
3342 + .with(Node::Region(
3343 + Slot::group("nested").with(Node::text("inside")),
3344 + )),
3345 + )
3346 + }
3347 +
3348 + #[test]
3349 + fn the_markup_is_not_a_function_of_the_width() {
3350 + // "Any width, one answer", `makeover-layout` 0.27.4. This renderer keeps
3351 + // the property the cheapest way there is: it is never told the width, so
3352 + // narrowing is the stylesheet's and lives in `@media`, where there is
3353 + // nowhere to keep the width you came from.
3354 + //
3355 + // Two halves. The first is that a reused renderer answers the same as a
3356 + // fresh one, which is what would fail the day someone memoised a track
3357 + // list on the `Webview`. The second is that the document carries no
3358 + // measurement at all, which is what would fail the day someone reached for
3359 + // an inline `style` to size something -- an inline width is a number
3360 + // computed once, and a number computed once is the previous frame's
3361 + // answer.
3362 + let screen = every_shape_that_could_cache();
3363 +
3364 + let reused = Webview::new();
3365 + let first = reused.screen(&screen);
3366 + for _ in 0..4 {
3367 + assert_eq!(reused.screen(&screen), first);
3368 + }
3369 + assert_eq!(Webview::new().screen(&screen), first);
3370 +
3371 + // No absolute length reaches the markup. The one inline style the
3372 + // document carries is the arrangement's share, and it is in `fr`: a
3373 + // proportion of whatever there turns out to be, which is a fact the
3374 + // description stated rather than a size this renderer worked out.
3375 + for style in first.split("style=\"").skip(1) {
3376 + let value = style.split('"').next().expect("an attribute closes");
3377 + for unit in ["px", "vw", "vh", "ch", "pt"] {
3378 + assert!(!value.contains(unit), "{unit} in {value}");
3379 + }
3380 + assert!(value.contains("fr"), "{value}");
3381 + }
3382 + }