nandi/jolt-nativepublic Fork 0
a7f62025fc9a5a5db4edb9a6ba6808dc31f7596b
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

tests.rs · 371 lines · 11.4 KBRust Blame HistoryRaw
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago1//! The backend end to end, with no terminal.
2//!
3//! Every one of these mounts real nodes through the same calls the ABI makes,
4//! paints a real frame, and asserts on the lines that came out — which is the
5//! whole reason painting goes through a grid. Nothing here needs a TTY, a
6//! display or raw mode, so it all runs in CI.
7
8use crate::screen::attr;
9use crate::tree::Value;
10use crate::ui::Ui;
11
12fn ui() -> Ui {
13 Ui::new(24, 8)
14}
15
16/// Mount `tag` under `parent` with the string props given.
17fn node(ui: &mut Ui, parent: u32, tag: &str, props: &[(&str, &str)]) -> u32 {
18 let id = ui.tree.new_node(tag);
19 for (key, value) in props {
20 ui.tree.set(id, key, Value::Str((*value).to_owned()));
21 }
22 ui.tree.append(parent, id);
23 id
24}
25
26fn events(ui: &mut Ui) -> Vec<(u32, String, String, f64)> {
27 let mut out = Vec::new();
28 while ui.tree.poll() {
29 let event = ui.tree.current().unwrap();
30 out.push((
31 event.node,
32 event.name.to_owned(),
33 event.text.clone(),
34 event.num,
35 ));
36 }
37 out
38}
39
40#[test]
41fn a_column_paints_its_children_down_the_page() {
42 let mut ui = ui();
43 let root = ui.tree.root();
44 node(&mut ui, root, "label", &[("label", "first")]);
45 node(&mut ui, root, "label", &[("label", "second")]);
46 ui.frame();
47 assert_eq!(ui.screen.line(0), "first");
48 assert_eq!(ui.screen.line(1), "second");
49}
50
51#[test]
52fn a_row_paints_its_children_across_with_its_spacing_between_them() {
53 let mut ui = ui();
54 let root = ui.tree.root();
55 let row = node(&mut ui, root, "hbox", &[("orientation", "horizontal")]);
56 ui.tree.set(row, "spacing", Value::Num(2.0));
57 node(&mut ui, row, "label", &[("label", "aa")]);
58 node(&mut ui, row, "label", &[("label", "bb")]);
59 ui.frame();
60 assert_eq!(ui.screen.line(0), "aa bb");
61}
62
63#[test]
64fn a_label_wraps_to_the_width_it_was_given() {
65 let mut ui = Ui::new(10, 4);
66 let root = ui.tree.root();
67 node(&mut ui, root, "label", &[("label", "one two three four")]);
68 ui.frame();
69 assert_eq!(ui.screen.line(0), "one two");
70 assert_eq!(ui.screen.line(1), "three four");
71}
72
73#[test]
74fn a_frame_draws_a_border_with_its_label_in_the_top_edge() {
75 let mut ui = Ui::new(12, 4);
76 let root = ui.tree.root();
77 let frame = node(&mut ui, root, "frame", &[("label", "Task")]);
78 node(&mut ui, frame, "label", &[("label", "hi")]);
79 ui.frame();
80 assert_eq!(ui.screen.line(0), "┌ Task ────┐");
81 assert_eq!(ui.screen.line(1), "│hi │");
82 assert_eq!(ui.screen.line(2), "└──────────┘");
83}
84
85#[test]
86fn the_first_focusable_widget_takes_focus_and_tab_walks_the_ring() {
87 let mut ui = ui();
88 let root = ui.tree.root();
89 let first = node(&mut ui, root, "button", &[("label", "one")]);
90 let second = node(&mut ui, root, "button", &[("label", "two")]);
91 ui.frame();
92 assert_eq!(ui.focus(), first);
93 ui.key("tab");
94 assert_eq!(ui.focus(), second);
95 ui.key("tab");
96 assert_eq!(ui.focus(), first, "the ring wraps");
97 ui.key("shift+tab");
98 assert_eq!(ui.focus(), second);
99}
100
101#[test]
102fn autofocus_beats_paint_order() {
103 let mut ui = ui();
104 let root = ui.tree.root();
105 node(&mut ui, root, "button", &[("label", "one")]);
106 let wanted = node(&mut ui, root, "entry", &[]);
107 ui.tree.set(wanted, "autofocus", Value::Bool(true));
108 ui.frame();
109 assert_eq!(ui.focus(), wanted);
110}
111
112#[test]
113fn a_focused_button_is_drawn_in_reverse_and_activates_on_enter() {
114 let mut ui = ui();
115 let root = ui.tree.root();
116 let button = node(&mut ui, root, "button", &[("label", "go")]);
117 ui.frame();
118 assert_eq!(ui.screen.line(0), "[ go ]");
119 assert!(ui.screen.cell(0, 0).unwrap().style.has(attr::REVERSE));
120 ui.key("enter");
121 assert_eq!(
122 events(&mut ui),
123 vec![(button, "click".into(), String::new(), 0.0)]
124 );
125}
126
127#[test]
128fn a_checkbutton_writes_its_new_state_back_as_well_as_reporting_it() {
129 let mut ui = ui();
130 let root = ui.tree.root();
131 let check = node(&mut ui, root, "checkbutton", &[("label", "live")]);
132 ui.frame();
133 assert_eq!(ui.screen.line(0), "[ ] live");
134 ui.key("space");
135 assert_eq!(
136 events(&mut ui),
137 vec![(check, "toggled".into(), String::new(), 1.0)]
138 );
139 // A caller that ignores the event still has a working control.
140 assert_eq!(ui.tree.props(check).bool("active", false), true);
141 ui.frame();
142 assert_eq!(ui.screen.line(0), "[x] live");
143}
144
145#[test]
146fn typing_in_an_entry_edits_its_text_and_reports_every_change() {
147 let mut ui = ui();
148 let root = ui.tree.root();
149 let entry = node(&mut ui, root, "entry", &[("placeholder", "name")]);
150 ui.frame();
151 assert_eq!(
152 ui.screen.line(0),
153 "name",
154 "the placeholder shows until it is typed in"
155 );
156 for key in ["h", "i", "space", "there"] {
157 for name in [key] {
158 if name.chars().count() > 1 && name != "space" {
159 for ch in name.chars() {
160 ui.key(&ch.to_string());
161 }
162 } else {
163 ui.key(name);
164 }
165 }
166 }
167 assert_eq!(ui.tree.props(entry).str("text"), "hi there");
168 let changes = events(&mut ui);
169 assert_eq!(changes.len(), 8);
170 assert_eq!(changes.last().unwrap().2, "hi there");
171 ui.frame();
172 assert_eq!(ui.screen.line(0), "hi there");
173}
174
175#[test]
176fn readline_keys_edit_where_the_caret_is() {
177 let mut ui = ui();
178 let root = ui.tree.root();
179 let entry = node(&mut ui, root, "entry", &[("text", "one two three")]);
180 ui.frame();
181 ui.key("ctrl+w");
182 assert_eq!(ui.tree.props(entry).str("text"), "one two ");
183 ui.key("ctrl+a");
184 ui.key("delete");
185 assert_eq!(ui.tree.props(entry).str("text"), "ne two ");
186 ui.key("ctrl+e");
187 ui.key("backspace");
188 assert_eq!(ui.tree.props(entry).str("text"), "ne two");
189 ui.key("ctrl+u");
190 assert_eq!(ui.tree.props(entry).str("text"), "");
191}
192
193#[test]
194fn enter_in_an_entry_activates_rather_than_typing() {
195 let mut ui = ui();
196 let root = ui.tree.root();
197 let entry = node(&mut ui, root, "entry", &[("text", "search")]);
198 ui.frame();
199 ui.key("enter");
200 assert_eq!(
201 events(&mut ui),
202 vec![(entry, "activate".into(), "search".into(), 0.0)]
203 );
204 assert_eq!(ui.tree.props(entry).str("text"), "search");
205}
206
207#[test]
208fn a_listbox_moves_its_cursor_with_the_arrows_and_with_j_and_k() {
209 let mut ui = ui();
210 let root = ui.tree.root();
211 let list = node(&mut ui, root, "listbox", &[]);
212 for name in ["alpha", "beta", "gamma"] {
213 node(&mut ui, list, "label", &[("label", name)]);
214 }
215 ui.frame();
216 assert_eq!(ui.screen.line(0), "› alpha");
217 ui.key("j");
218 assert_eq!(
219 events(&mut ui),
220 vec![(list, "select".into(), "beta".into(), 1.0)]
221 );
222 ui.key("G");
223 assert_eq!(ui.tree.props(list).num("selected", -1.0), 2.0);
224 ui.key("enter");
225 assert_eq!(
226 events(&mut ui),
227 vec![
228 (list, "select".into(), "gamma".into(), 2.0),
229 (list, "activate".into(), "gamma".into(), 2.0)
230 ]
231 );
232 ui.frame();
233 assert_eq!(ui.screen.line(2), "› gamma");
234}
235
236#[test]
237fn a_key_nothing_wanted_reaches_the_caller_as_an_event() {
238 let mut ui = ui();
239 let root = ui.tree.root();
240 let button = node(&mut ui, root, "button", &[("label", "go")]);
241 ui.frame();
242 assert!(!ui.key("f5"));
243 assert_eq!(
244 events(&mut ui),
245 vec![(button, "key".into(), "f5".into(), 0.0)]
246 );
247}
248
249#[test]
250fn ctrl_c_asks_the_loop_to_stop() {
251 let mut ui = ui();
252 assert!(!ui.should_close());
253 ui.key("ctrl+c");
254 assert!(ui.should_close());
255}
256
257#[test]
258fn an_insensitive_subtree_is_dimmed_and_out_of_the_focus_ring() {
259 let mut ui = ui();
260 let root = ui.tree.root();
261 let column = node(&mut ui, root, "vbox", &[]);
262 ui.tree.set(column, "sensitive", Value::Bool(false));
263 node(&mut ui, column, "button", &[("label", "go")]);
264 let live = node(&mut ui, root, "button", &[("label", "live")]);
265 ui.frame();
266 assert!(ui.screen.cell(0, 0).unwrap().style.has(attr::DIM));
267 assert_eq!(ui.focus(), live);
268}
269
270#[test]
271fn a_click_focuses_and_activates_what_is_under_it() {
272 let mut ui = ui();
273 let root = ui.tree.root();
274 node(&mut ui, root, "button", &[("label", "one")]);
275 let second = node(&mut ui, root, "button", &[("label", "two")]);
276 ui.frame();
277 assert!(ui.click(2, 1));
278 assert_eq!(ui.focus(), second);
279 assert_eq!(
280 events(&mut ui),
281 vec![(second, "click".into(), String::new(), 0.0)]
282 );
283 assert!(!ui.click(20, 7), "a click on nothing is not an event");
284}
285
286#[test]
287fn a_scroll_shows_a_window_of_its_content_and_will_not_go_past_the_end() {
288 let mut ui = Ui::new(10, 3);
289 let root = ui.tree.root();
290 let scroll = node(&mut ui, root, "scroll", &[]);
291 for i in 0..6 {
292 node(&mut ui, scroll, "label", &[("label", &format!("row {i}"))]);
293 }
294 ui.frame();
295 assert_eq!(ui.screen.lines(), vec!["row 0", "row 1", "row 2"]);
296 ui.wheel(1, 1, 2);
297 ui.frame();
298 assert_eq!(ui.screen.lines(), vec!["row 2", "row 3", "row 4"]);
299 ui.wheel(1, 1, 40);
300 ui.frame();
301 assert_eq!(ui.screen.lines(), vec!["row 3", "row 4", "row 5"]);
302 // The clamped viewport is written back, so the next scroll up starts from
303 // where the reader actually is.
304 assert_eq!(ui.tree.props(scroll).num("offset", -1.0), 3.0);
305}
306
307#[test]
308fn an_overlay_floats_in_the_middle_over_whatever_was_under_it() {
309 let mut ui = Ui::new(14, 5);
310 let root = ui.tree.root();
311 node(&mut ui, root, "label", &[("label", "beneath")]);
312 let overlay = node(&mut ui, root, "overlay", &[("label", "Sure?")]);
313 node(&mut ui, overlay, "label", &[("label", "yes")]);
314 ui.frame();
315 assert_eq!(ui.screen.line(1), " ┌ Sure? ┐");
316 assert_eq!(ui.screen.line(2), " │yes │");
317 ui.key("esc");
318 assert_eq!(
319 events(&mut ui),
320 vec![(overlay, "close".into(), String::new(), 0.0)]
321 );
322}
323
324#[test]
325fn a_progress_bar_fills_the_share_of_its_width_it_was_given() {
326 let mut ui = Ui::new(10, 2);
327 let root = ui.tree.root();
328 let bar = node(&mut ui, root, "progress", &[]);
329 ui.tree.set(bar, "value", Value::Num(0.5));
330 ui.frame();
331 assert_eq!(ui.screen.line(0), "█████░░░░░");
332}
333
334#[test]
335fn colours_and_attributes_are_inherited_by_a_subtree() {
336 let mut ui = ui();
337 let root = ui.tree.root();
338 let column = node(&mut ui, root, "vbox", &[("color", "red")]);
339 ui.tree.set(column, "bold", Value::Bool(true));
340 node(&mut ui, column, "label", &[("label", "hi")]);
341 ui.frame();
342 let cell = ui.screen.cell(0, 0).unwrap();
343 assert_eq!(cell.style.fg, crate::screen::Color::Indexed(1));
344 assert!(cell.style.has(attr::BOLD));
345}
346
347#[test]
348fn an_unknown_tag_still_shows_its_children() {
349 let mut ui = ui();
350 let root = ui.tree.root();
351 let odd = node(&mut ui, root, "sparkline", &[]);
352 node(&mut ui, odd, "label", &[("label", "inside")]);
353 ui.frame();
354 assert_eq!(ui.screen.line(0), "inside");
355}
356
357#[test]
358fn focus_survives_a_repaint_and_lands_somewhere_when_its_widget_is_unmounted() {
359 let mut ui = ui();
360 let root = ui.tree.root();
361 let first = node(&mut ui, root, "button", &[("label", "one")]);
362 let second = node(&mut ui, root, "button", &[("label", "two")]);
363 ui.frame();
364 ui.key("tab");
365 assert_eq!(ui.focus(), second);
366 ui.frame();
367 assert_eq!(ui.focus(), second, "a repaint does not move the focus");
368 ui.tree.remove(root, second);
369 ui.frame();
370 assert_eq!(ui.focus(), first);
371}