Skip to main content

max / quasi

Test every Node member through both renderer walks quasi-immediate's exhaustiveness broke once already and its walk kept compiling. The other two renderers had no equivalent backstop: quasi-tui walks Node twice, through draw and height, and quasi-webview once through node_html, and nothing proved either walk knew the whole vocabulary. Each crate now owns a one_of_everything list plus a count test, so a member added upstream fails the suite rather than reaching a catch-all.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-23 20:49 UTC
Signed with PGP, not checked
Commit: 8d9977fcf8af7e5294a5e0139b6e152a28cd4ffa
Parent: 7fd2630
3 files changed, +171 insertions, -4 deletions
M Cargo.lock +4 -4
@@ -6197,10 +6197,6 @@
6197 6197 "winnow 1.0.4",
6198 6198 ]
6199 6199
6200 - [[patch.unused]]
6201 - name = "quasi-type"
6202 - version = "0.1.0"
6203 -
6204 6200 [[patch.unused]]
6205 6201 name = "kberg"
6206 6202 version = "0.1.0"
@@ -6217,6 +6213,10 @@
6217 6213 name = "tagtree"
6218 6214 version = "0.4.1"
6219 6215
6216 + [[patch.unused]]
6217 + name = "quasi-type"
6218 + version = "0.1.0"
6219 +
6220 6220 [[patch.unused]]
6221 6221 name = "synckit-client"
6222 6222 version = "0.8.1"
@@ -122,6 +122,94 @@
122 122 ("Settings", layout::Priority::Essential),
123 123 ];
124 124
125 + /// One of every `Node` member, in declaration order.
126 + ///
127 + /// Kept as a function so more than one test can walk it, and it has to stay
128 + /// complete: `Node` is `#[non_exhaustive]`, so a member added upstream lands on
129 + /// a catch-all arm and compiles. This list plus the count below is what says a
130 + /// walk has learned the member rather than merely accepting it.
131 + fn one_of_everything() -> Vec<Node> {
132 + vec![
133 + Node::page("Tasks"),
134 + Node::text("plain"),
135 + Node::rich("**bold** and `code`"),
136 + Node::Act(Act::new("Save", Action::post("/save"))),
137 + Node::Link {
138 + text: "Docs".to_owned(),
139 + action: Action::get("/docs"),
140 + },
141 + Node::Figure(Figure::new("17", "Streak")),
142 + Node::since(std::time::SystemTime::UNIX_EPOCH),
143 + Node::until(std::time::SystemTime::UNIX_EPOCH),
144 + Node::age(std::time::SystemTime::UNIX_EPOCH),
145 + Node::Image(quasi_router::Picture::new("/cover.png", "The library view")),
146 + Node::Token(Tag::badge("beta")),
147 + Node::banner(layout::Tone::Info, "Saved"),
148 + Node::empty("Nothing here yet"),
149 + Node::Field(Box::new(Field::new(
150 + layout::FieldKind::Text,
151 + "title",
152 + "Title",
153 + ))),
154 + Node::Form {
155 + action: Action::post("/save"),
156 + submit: "Save".to_owned(),
157 + fields: vec![Field::new(layout::FieldKind::Text, "title", "Title")],
158 + },
159 + Node::list([Row::new("One")]),
160 + Node::Table {
161 + columns: vec![Column::new("Name")],
162 + rows: vec![Cells::new([Cell::new("One")])],
163 + more: None,
164 + },
165 + Node::Timeline {
166 + track: layout::Track::DAY,
167 + entries: vec![quasi_router::Placed::new(540, 45, Row::new("Standup"))],
168 + focus: Some(540),
169 + },
170 + Node::Select {
171 + kind: layout::Selector::Tabs,
172 + options: vec![(Choice::new("all", "All"), None)],
173 + chosen: Some("all".to_owned()),
174 + action: Some(Action::get("/filter")),
175 + },
176 + Node::Meter(Meter::new(3, 6)),
177 + Node::stats([Figure::new("17", "Streak")]),
178 + Node::Region(Slot::new("nested", RegionKind::Pane)),
179 + ]
180 + }
181 +
182 + #[test]
183 + fn every_described_node_draws_without_panicking() {
184 + // This crate walks `Node` twice, so the exhaustiveness it claims is two
185 + // claims. Drawing is the first of them: one of everything, painted.
186 + for node in one_of_everything() {
187 + let _ = drawn(&node, 60, 12);
188 + }
189 + }
190 +
191 + #[test]
192 + fn every_described_node_is_measured_without_panicking() {
193 + // The second walk, and the one that has no visible symptom when it drifts:
194 + // a member `height` does not know is measured as something else and the
195 + // rows below it land in the wrong place.
196 + let tui = tui();
197 + for node in one_of_everything() {
198 + let _ = tui.height(&node, 60);
199 + }
200 + }
201 +
202 + #[test]
203 + fn the_exhaustiveness_list_holds_one_of_every_member() {
204 + // A count rather than a comment. `Node` cannot be iterated, so nothing but
205 + // this stops the list above going stale while both walks keep compiling.
206 + assert_eq!(
207 + one_of_everything().len(),
208 + 22,
209 + "one of every `Node` member, in declaration order"
210 + );
211 + }
212 +
125 213 #[test]
126 214 fn a_member_of_a_run_is_drawn_at_all() {
127 215 // The defect: `draw` and `height` both walked `body` and nothing walked
@@ -46,6 +46,85 @@
46 46 Webview::new().fragment(node)
47 47 }
48 48
49 + /// One of every `Node` member, in declaration order.
50 + ///
51 + /// Kept as a function so more than one test can walk it, and it has to stay
52 + /// complete: `Node` is `#[non_exhaustive]`, so a member added upstream lands on
53 + /// a catch-all arm and compiles. This list plus the count below is what says
54 + /// `node_html` has learned the member rather than merely accepting it.
55 + fn one_of_everything() -> Vec<Node> {
56 + vec![
57 + Node::page("Tasks"),
58 + Node::text("plain"),
59 + Node::rich("**bold** and `code`"),
60 + Node::Act(Act::new("Save", Action::post("/save"))),
61 + Node::Link {
62 + text: "Docs".to_owned(),
63 + action: Action::get("/docs"),
64 + },
65 + Node::Figure(Figure::new("17", "Streak")),
66 + Node::since(std::time::SystemTime::UNIX_EPOCH),
67 + Node::until(std::time::SystemTime::UNIX_EPOCH),
68 + Node::age(std::time::SystemTime::UNIX_EPOCH),
69 + Node::Image(quasi_router::Picture::new("/cover.png", "The library view")),
70 + Node::Token(Tag::badge("beta")),
71 + Node::banner(layout::Tone::Info, "Saved"),
72 + Node::empty("Nothing here yet"),
73 + Node::Field(Box::new(Field::new(
74 + layout::FieldKind::Text,
75 + "title",
76 + "Title",
77 + ))),
78 + Node::Form {
79 + action: Action::post("/save"),
80 + submit: "Save".to_owned(),
81 + fields: vec![Field::new(layout::FieldKind::Text, "title", "Title")],
82 + },
83 + Node::list([Row::new("One")]),
84 + Node::Table {
85 + columns: vec![Column::new("Name")],
86 + rows: vec![Cells::new([Cell::new("One")])],
87 + more: None,
88 + },
89 + Node::Timeline {
90 + track: layout::Track::DAY,
91 + entries: vec![quasi_router::Placed::new(540, 45, Row::new("Standup"))],
92 + focus: Some(540),
93 + },
94 + Node::Select {
95 + kind: layout::Selector::Tabs,
96 + options: vec![(Choice::new("all", "All"), None)],
97 + chosen: Some("all".to_owned()),
98 + action: Some(Action::get("/filter")),
99 + },
100 + Node::Meter(Meter::new(3, 6)),
101 + Node::stats([Figure::new("17", "Streak")]),
102 + Node::Region(Slot::new("nested", RegionKind::Pane)),
103 + ]
104 + }
105 +
106 + #[test]
107 + fn every_described_node_emits_markup() {
108 + // One of everything, through `node_html`. A member the walk does not know
109 + // reaches a catch-all rather than a compile error, so an empty fragment is
110 + // what silence looks like here and this is what objects to it.
111 + for node in one_of_everything() {
112 + let html = fragment(&node);
113 + assert!(!html.is_empty(), "nothing came out for {node:?}");
114 + }
115 + }
116 +
117 + #[test]
118 + fn the_exhaustiveness_list_holds_one_of_every_member() {
119 + // A count rather than a comment. `Node` cannot be iterated, so nothing but
120 + // this stops the list above going stale while the walk keeps compiling.
121 + assert_eq!(
122 + one_of_everything().len(),
123 + 22,
124 + "one of every `Node` member, in declaration order"
125 + );
126 + }
127 +
49 128 #[test]
50 129 fn a_screen_is_a_whole_document() {
51 130 let html = render(&Screen::list_detail("Tasks", false));