//! What egui draws from a description. //! //! Assertions are over what came back rather than over pixels: egui's test //! harness lays a real `Ui` out and answers real `Response`s, so what a renderer //! owes is that pressing a described control produces the described request. //! What it looks like is the palette's answer and changes with it. use egui::Color32; use makeover_immediate::Palette; use quasi_router::{ Act, Action, Address, Chrome, Consult, Field, Frame, Message, Method, Node, Outcome, RegionKind, Request, Response, Row, Run, Screen, Slot, layout, }; use crate::view::Asking; use crate::{Immediate, Runtime, Step, View, runtime::types}; fn palette() -> Palette { Palette { page: Color32::from_rgb(1, 1, 1), raised: Color32::from_rgb(2, 2, 2), overlay: Color32::from_rgb(3, 3, 3), well: Color32::from_rgb(4, 4, 4), sunken: Color32::from_rgb(5, 5, 5), bevel_light: Color32::WHITE, bevel_dark: Color32::BLACK, elevation: Color32::from_black_alpha(46), content: Color32::from_rgb(6, 6, 6), content_secondary: Color32::from_rgb(66, 66, 66), content_muted: Color32::from_rgb(7, 7, 7), action: Color32::from_rgb(8, 8, 8), danger: Color32::from_rgb(9, 9, 9), success: Color32::from_rgb(10, 10, 10), warning: Color32::from_rgb(11, 11, 11), info: Color32::from_rgb(12, 12, 12), } } fn renderer() -> Immediate { Immediate::new(palette()) } /// A screen with one region holding these nodes. fn screen_of(nodes: impl IntoIterator) -> Screen { Screen::sidebar_content("Test").with( nodes .into_iter() .fold(Slot::new("main", RegionKind::Pane), Slot::with), ) } /// Draw a screen once, with nothing pressed. fn draw(screen: &Screen, view: &mut View) { let immediate = renderer(); egui::__run_test_ui(|ui| { immediate.screen(ui, screen, view); }); } /// One of every `Node` member, in declaration order. /// /// The list this test asserts against, kept as a function so more than one test /// can walk it. It has to stay complete: it is the only thing standing between a /// member added upstream and a renderer that draws less than it describes, /// because the compiler's half of that guarantee has been defeated once already. fn one_of_everything() -> Vec { vec![ Node::page("Tasks"), Node::text("plain"), Node::rich("**bold** and `code`"), Node::Act(Act::new("Save", Action::post("/save"))), Node::Link { text: "Docs".to_owned(), action: Action::get("/docs"), }, Node::Figure(quasi_router::Figure::new("17", "Streak")), Node::since(std::time::SystemTime::UNIX_EPOCH), Node::until(std::time::SystemTime::UNIX_EPOCH), Node::age(std::time::SystemTime::UNIX_EPOCH), Node::Image(quasi_router::Image::new("/cover.png", "The library view")), Node::Token(quasi_router::Tag::badge("beta")), Node::banner(layout::Tone::Info, "Saved"), Node::empty("Nothing here yet"), Node::Field(Box::new(Field::new( layout::FieldKind::Text, "title", "Title", ))), Node::Form { marks: ::quasi_router::stage::Marks::none(), action: Action::post("/save"), submit: "Save".to_owned(), fields: vec![Field::new(layout::FieldKind::Text, "title", "Title")], }, Node::list([quasi_router::Row::new("One")]), Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: vec![quasi_router::Column::new("Name")], rows: vec![quasi_router::Row::cells([quasi_router::Cell::new("One")])], more: None, }, Node::Timeline { marks: ::quasi_router::stage::Marks::none(), track: layout::Track::DAY, entries: vec![quasi_router::Placed::new( 540, 45, quasi_router::Row::new("Standup"), )], focus: Some(540), }, Node::Meter(quasi_router::Meter::new(3, 6)), Node::stats([quasi_router::Figure::new("17", "Streak")]), Node::Region(Slot::new("nested", RegionKind::Pane)), ] } #[test] fn every_described_node_draws_without_panicking() { // The walk is exhaustive over `Node`, so this is the assertion that the // exhaustiveness is real rather than a match that compiles: one of // everything, drawn. // // It was neither, until 2026-08-15. `draw`'s last arm was a wildcard // forwarding to `container`, and this list was nine members short, so // `Node::Image` (0.4.0) and `Node::Timeline` (0.6.0) arrived, compiled and // panicked on `container`'s `unreachable!` with nothing objecting. Both // halves are fixed and the list is complete; keeping it complete is what // this test is for. let mut view = View::new(); let screen = screen_of(one_of_everything()); draw(&screen, &mut view); } #[test] fn the_exhaustiveness_list_holds_one_of_every_member() { // A count rather than a comment. `Node` cannot be iterated, so nothing but // this stops the list above going stale the way it did through two releases. // // `Node` took `#[non_exhaustive]` in 0.10.0, which makes this the only // guard rather than the second one: a member added upstream now lands on // `draw`'s catch-all and compiles, so the count is what says the list has // not learned it yet. assert_eq!( one_of_everything().len(), 21, "one of every `Node` member, in declaration order" ); } #[test] fn a_picture_draws_at_its_own_proportions_and_never_wider_than_the_box() { // The dimensions are the whole of what holds a picture's place: without // them the box is empty until the texture lands and then shoves everything // below it down the screen. Never scaled up, since a 5120-wide screenshot // is not asking for a 5120-wide window. let mut view = View::new(); let picture = quasi_router::Image::new("/wide.png", "A wide screenshot") .intrinsic(5120, 2560) .caption("The library view"); draw(&screen_of([Node::Image(picture)]), &mut view); // A decorative picture says nothing, so nothing stands in for it. The // difference between an empty `alt` and a missing one is a claim, and this // is the arm that reads it. draw( &screen_of([Node::Image(quasi_router::Image::new("/rule.png", ""))]), &mut view, ); } #[test] fn a_track_draws_whatever_unit_it_counts() { // The geometry is unit-agnostic and was correct while the ruler printed // `00:00` over a month strip, which is the defect `layout::Unit` closed. // Both units draw here so a day strip is exercised rather than assumed. let mut view = View::new(); let day = Node::Timeline { marks: ::quasi_router::stage::Marks::none(), track: layout::Track::DAY, entries: vec![ quasi_router::Placed::new(540, 45, quasi_router::Row::new("Standup")), // Overlapping, so the lane packing runs rather than sitting at one // lane for every entry. quasi_router::Placed::new(555, 60, quasi_router::Row::new("Review")), // Past the end of the span, which `Track::fraction` clamps rather // than drawing off the axis: an event running past midnight is a // real thing. quasi_router::Placed::new(1380, 180, quasi_router::Row::new("Late")), ], focus: Some(540), }; let strip = Node::Timeline { marks: ::quasi_router::stage::Marks::none(), track: layout::Track::days(layout::Span::new(0, 31)), entries: vec![quasi_router::Placed::new( 3, 5, quasi_router::Row::new("Leave"), )], focus: None, }; draw(&screen_of([day, strip]), &mut view); } #[test] fn a_track_with_no_ticks_still_draws() { // `Track::tick` of zero means an unlabelled axis, and a `slot` of zero // reads as one slot spanning the whole thing rather than a division by // zero. Both are documented upstream and both reach arithmetic here. let mut view = View::new(); let node = Node::Timeline { marks: ::quasi_router::stage::Marks::none(), track: layout::Track { span: layout::Span::DAY, slot: 0, tick: 0, unit: layout::Unit::Minutes, }, entries: vec![quasi_router::Placed::new( 0, 1, quasi_router::Row::new("All day"), )], focus: None, }; draw(&screen_of([node]), &mut view); } #[test] fn a_screen_with_no_press_asks_for_nothing() { // egui redraws continuously, so the ordinary frame is a user doing nothing. // A renderer answering a request per frame would call the router sixty // times a second. let immediate = renderer(); let screen = screen_of([Node::Act(Act::new("Save", Action::post("/save")))]); let mut view = View::new(); egui::__run_test_ui(|ui| { assert!(immediate.screen(ui, &screen, &mut view).is_none()); }); } #[test] fn what_is_typed_lives_in_the_view_and_not_in_the_description() { // The one thing egui does not hold for this renderer: a described field is // rebuilt every frame, so the buffer behind it has to outlive the frame. let mut view = View::new(); view.set("title", "hello"); assert_eq!(view.showing("title", Some("described")), "hello"); // Untouched reads the description; cleared does not. assert_eq!(view.showing("other", Some("described")), "described"); view.set("other", ""); assert_eq!(view.showing("other", Some("described")), ""); } #[test] fn a_form_submits_every_name_it_declared() { // A form that omits an untouched field is a form that cannot clear one. let mut view = View::new(); view.set("title", "typed"); let described = [("body".to_owned(), "offered".to_owned())] .into_iter() .collect(); let params = view.submission( &["title".to_owned(), "body".to_owned(), "empty".to_owned()], &described, ); assert_eq!(params.get("title"), Some("typed")); assert_eq!(params.get("body"), Some("offered")); assert_eq!(params.get("empty"), Some("")); } #[test] fn a_tick_is_the_views_and_the_description_only_seeds_it() { // After arrival the user's ticks are the truth, which is why seeding is // applied once rather than read on every draw. let mut row = quasi_router::Row::new("One"); row.selected = Some(true); row.value = Some("1".to_owned()); let screen = screen_of([Node::list([row])]); let mut view = View::new(); view.seed(&screen); assert!(view.is_ticked("1")); view.tick("1"); assert!( !view.is_ticked("1"), "the user untocked it and it stayed off" ); } #[test] fn a_fresh_runtime_has_nowhere_to_reload_from() { // Built from a screen rather than from an address, so there is no request // behind the opening screen to ask again. Idle rather than a guess. let runtime = Runtime::new(screen_of([Node::text("opening")])); assert_eq!(runtime.here(), None); assert_eq!(runtime.reload(), Step::Idle); } #[test] fn reload_asks_for_the_screen_showing_now() { let mut runtime = Runtime::new(screen_of([Node::text("opening")])); runtime.apply( &Request::get("/export"), Response { outcome: Outcome::Screen(screen_of([Node::text("configuring")])), notice: None, address: None, invalidates: Vec::new(), }, ); assert_eq!(runtime.reload(), Step::Call(Request::get("/export"))); } #[test] fn reloading_repeatedly_does_not_pile_up_history() { // The failure this guards: `remember` pushes where you were whenever a read // answers a screen, and a reload is a read answering the screen you are on. // Without the guard, a host refreshing a progress screen every frame builds // a history stack of that same screen and `back` walks through it. let mut runtime = Runtime::new(screen_of([Node::text("opening")])); runtime.apply( &Request::get("/one"), Response { outcome: Outcome::Screen(screen_of([Node::text("one")])), notice: None, address: None, invalidates: Vec::new(), }, ); runtime.apply( &Request::get("/two"), Response { outcome: Outcome::Screen(screen_of([Node::text("two")])), notice: None, address: None, invalidates: Vec::new(), }, ); for _ in 0..5 { let Step::Call(request) = runtime.reload() else { panic!("a screen that was navigated to can be asked for again"); }; runtime.apply( &request, Response { outcome: Outcome::Screen(screen_of([Node::text("two, again")])), notice: None, address: None, invalidates: Vec::new(), }, ); } // One step back is /one, and the next has nowhere to go: the five reloads // left history exactly as the two navigations did. assert_eq!(runtime.back(), Step::Call(Request::get("/one"))); assert_eq!(runtime.back(), Step::Idle); } #[test] fn a_reload_keeps_what_the_user_is_in_the_middle_of_typing() { // The failure this guards, and it is the one that decides whether `reload` // is usable at all: a refresh goes through `Outcome::Screen`, which resets // the view on arrival. A host reloading a form every frame would clear the // box under the caret sixty times a second. let mut runtime = Runtime::new(screen_of([Node::text("opening")])); let form = || { screen_of([Node::Field(Box::new(Field::new( layout::FieldKind::Text, "naming-pattern", "Naming pattern", )))]) }; runtime.apply( &Request::get("/export"), Response { outcome: Outcome::Screen(form()), notice: None, address: None, invalidates: Vec::new(), }, ); runtime.view_mut().set("naming-pattern", "{name}-{bpm}"); // Five refreshes, which is what a host reloading every frame does. for _ in 0..5 { let Step::Call(request) = runtime.reload() else { panic!("a screen that was navigated to can be asked for again"); }; runtime.apply( &request, Response { outcome: Outcome::Screen(form()), notice: None, address: None, invalidates: Vec::new(), }, ); } assert_eq!( runtime.view().edit("naming-pattern"), Some("{name}-{bpm}"), "a refresh is not an arrival, so it does not clear the box being typed into" ); // Going somewhere else is a different matter, and still clears. runtime.apply( &Request::get("/settings"), Response { outcome: Outcome::Screen(screen_of([Node::text("elsewhere")])), notice: None, address: None, invalidates: Vec::new(), }, ); assert_eq!(runtime.view().edit("naming-pattern"), None); } #[test] fn a_reload_does_not_put_back_a_tick_the_user_took_off() { // `View::seed` is arrival behaviour by its own documentation -- "after this // the user's ticks are the truth" -- so a refresh must not run it. Otherwise // unticking a row that the description says is ticked lasts exactly until // the next reload. let ticked = || { let mut row = quasi_router::Row::new("One"); row.selected = Some(true); row.value = Some("1".to_owned()); screen_of([Node::list([row])]) }; let mut runtime = Runtime::new(screen_of([Node::text("opening")])); runtime.apply( &Request::get("/files"), Response { outcome: Outcome::Screen(ticked()), notice: None, address: None, invalidates: Vec::new(), }, ); // Arrival seeded it, which is the behaviour being distinguished from. assert!(runtime.view().is_ticked("1")); runtime.view_mut().tick("1"); let Step::Call(request) = runtime.reload() else { panic!("a screen that was navigated to can be asked for again"); }; runtime.apply( &request, Response { outcome: Outcome::Screen(ticked()), notice: None, address: None, invalidates: Vec::new(), }, ); assert!( !runtime.view().is_ticked("1"), "the user took it off and a refresh is not an arrival" ); } #[test] fn an_overlay_is_not_a_place_and_dismissing_it_reveals_what_was_under_it() { let mut runtime = Runtime::new(screen_of([Node::text("underneath")])); // Two navigations, because history holds where you *were*: the first // records where we are and the second pushes it behind us. for path in ["/two", "/three"] { runtime.apply( &Request::get(path), Response { outcome: Outcome::Screen(screen_of([Node::text("a place")])), notice: None, address: None, invalidates: Vec::new(), }, ); } runtime.apply( &Request::get("/palette"), Response { outcome: Outcome::Over(screen_of([Node::text("palette")])), notice: None, address: None, invalidates: Vec::new(), }, ); assert!(runtime.overlaid()); assert_eq!(runtime.screen().title, "Test"); // Dismissing reveals rather than navigates, so history is untouched and // still has somewhere to go afterwards. assert!(matches!(runtime.back(), Step::Call(_))); } #[test] fn a_navigation_takes_the_overlay_with_it() { // Arriving somewhere new with a palette still floating over it is the state // nobody asked for. let mut runtime = Runtime::new(screen_of([Node::text("first")])); runtime.apply( &Request::get("/palette"), Response { outcome: Outcome::Over(screen_of([Node::text("palette")])), notice: None, address: None, invalidates: Vec::new(), }, ); assert!(runtime.overlaid()); runtime.apply( &Request::get("/two"), Response { outcome: Outcome::Screen(screen_of([Node::text("second")])), notice: None, address: None, invalidates: Vec::new(), }, ); assert!(!runtime.overlaid()); } #[test] fn a_write_is_not_a_place_and_a_read_of_a_screen_is() { let mut runtime = Runtime::new(screen_of([Node::text("first")])); for path in ["/two", "/three"] { runtime.apply( &Request::get(path), Response { outcome: Outcome::Screen(screen_of([Node::text("place")])), notice: None, address: None, invalidates: Vec::new(), }, ); } // A write answering a screen is not somewhere to come back to. runtime.apply( &Request::post("/save"), Response { outcome: Outcome::Screen(screen_of([Node::text("saved")])), notice: None, address: None, invalidates: Vec::new(), }, ); match runtime.back() { Step::Call(request) => assert_eq!(request.path, "/two"), other => panic!("expected the place behind, got {other:?}"), } } #[test] fn an_address_the_router_named_overrides_the_derivation() { let mut runtime = Runtime::new(screen_of([Node::text("first")])); runtime.apply( &Request::get("/one"), Response { outcome: Outcome::Screen(screen_of([Node::text("one")])), notice: None, address: None, invalidates: Vec::new(), }, ); // `Unchanged` says this read is not a place, so nothing is pushed behind it. runtime.apply( &Request::get("/transient"), Response { outcome: Outcome::Screen(screen_of([Node::text("transient")])), notice: None, address: Some(Address::Unchanged), invalidates: Vec::new(), }, ); assert!(matches!(runtime.back(), Step::Idle)); } #[test] fn a_fragment_naming_a_region_that_is_not_there_says_so() { // A terminal and a window can both say it, where a webview swallows it: // drawing nothing would look like a control that does nothing. let mut runtime = Runtime::new(screen_of([Node::text("first")])); runtime.apply( &Request::get("/x"), Response { outcome: Outcome::Fragment { region: "nowhere".to_owned(), node: Node::text("new"), }, notice: None, address: None, invalidates: Vec::new(), }, ); let said = runtime .screen() .notices .iter() .any(|node| matches!(node, Node::Notice { text, .. } if text.contains("nowhere"))); assert!(said, "the missing region was not reported"); } #[test] fn what_a_response_says_lands_on_the_screen_it_belongs_to() { let mut runtime = Runtime::new(screen_of([Node::text("first")])); runtime.apply( &Request::post("/save"), Response { outcome: Outcome::Screen(screen_of([Node::text("second")])), notice: Some(Message { kind: layout::Notice::Toast, tone: layout::Tone::Success, text: "Saved".to_owned(), undo: None, }), address: None, invalidates: Vec::new(), }, ); assert!( runtime .screen() .notices .iter() .any(|node| matches!(node, Node::Notice { text, .. } if text == "Saved")), "the notice did not arrive with the screen it belongs to" ); } #[test] fn the_way_back_a_response_offered_survives_the_conversion() { // `bde35298`. This host keeps a screen and converts a `Message` into a // `Node::Notice`, so it is the one that has to carry the undo across; // until the node grew an act it dropped it, exactly as quasi-tui did. let mut runtime = Runtime::new(screen_of([Node::text("first")])); runtime.apply( &Request::post("/tasks/7/delete"), Response { outcome: Outcome::Screen(screen_of([Node::text("second")])), notice: Some(Message { kind: layout::Notice::Toast, tone: layout::Tone::Success, text: "Deleted".to_owned(), undo: Some(Action::post("/tasks/7/restore")), }), address: None, invalidates: Vec::new(), }, ); let notice = runtime.screen().notices.first().expect("a notice arrived"); let Node::Notice { act: Some(act), .. } = notice else { panic!("the undo did not survive: {notice:?}"); }; assert_eq!(act.label, Message::UNDO); assert_eq!(act.action.route(), Some("/tasks/7/restore")); } #[test] fn pressing_a_notices_undo_calls_the_route_the_response_named() { // Drawn and pressable, which is the half the conversion alone does not // buy: a notice was a leaf here, drawn through a walk with no `Pass` to // fire through, so an act on one had to move it out of that walk. let mut host = Host::new(); let screen = screen_of([Node::text("here")]).saying( Node::toast(layout::Tone::Success, "Deleted").about(quasi_router::Act::new( "Undo", Action::post("/tasks/7/restore"), )), ); host.settle(&screen); assert!(on_screen(&host, "Deleted"), "the notice is not drawn"); let fired = host.click(&screen, "Undo"); assert_eq!( fired.and_then(|fired| fired.action.route().map(str::to_owned)), Some("/tasks/7/restore".to_owned()), "the undo is on the screen and does nothing" ); } /// Press the control painted with this label, on a runtime, and answer its step. /// /// `Host` drives the renderer and this drives the `Runtime` around it, which is /// the difference that matters for a destination the runtime performs rather /// than reports: the fired action never leaves the runtime, so a test that /// stopped at `Fired` would assert what was described rather than what happened. /// /// The first frame is drawn only to find out where the label landed, which is /// `Host::find`'s trick with the runtime holding the screen. fn runtime_press(runtime: &mut Runtime, immediate: &Immediate, label: &str) -> Step { let ctx = egui::Context::default(); let input = |events: Vec| egui::RawInput { screen_rect: Some(egui::Rect::from_min_size( egui::Pos2::ZERO, egui::vec2(900.0, 700.0), )), events, ..Default::default() }; let mut found = None; let output = ctx.clone().run_ui(input(Vec::new()), |ui| { runtime.show(ui, immediate); }); fn walk(shape: &egui::Shape, text: &str, found: &mut Option) { match shape { egui::Shape::Text(t) if t.galley.job.text.contains(text) => { *found = Some(egui::Rect::from_min_size(t.pos, t.galley.size())); } egui::Shape::Vec(shapes) => { for shape in shapes { walk(shape, text, found); } } _ => {} } } for clipped in &output.shapes { walk(&clipped.shape, label, &mut found); } let pos = found .unwrap_or_else(|| panic!("nothing painted {label:?}; the press has nowhere to land")) .center(); let press = |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE, }; let mut step = Step::Idle; for events in [ vec![egui::Event::PointerMoved(pos)], vec![egui::Event::PointerMoved(pos), press(true)], vec![egui::Event::PointerMoved(pos), press(false)], ] { let mut this = Step::Idle; let _ = ctx.clone().run_ui(input(events), |ui| { this = runtime.show(ui, immediate); }); if !matches!(this, Step::Idle) { step = this; } } step } #[test] fn a_control_that_goes_back_asks_for_where_the_reader_came_from() { // `33c27e81`. The address is this runtime's history and not anything the // description could have named, which is the whole reason it is a // destination rather than a path some screen computes. let mut runtime = Runtime::new(screen_of([Node::text("the list")])); runtime.apply( &Request::get("/tasks"), Response::screen(screen_of([Node::text("the list")])), ); runtime.apply( &Request::get("/settings"), Response::screen(screen_of([Node::Act(Act::new("Close", Action::back()))])), ); assert_eq!( runtime_press(&mut runtime, &renderer(), "Close"), Step::Call(Request::get("/tasks")), "back did not ask for the place before this one" ); } #[test] fn back_is_not_local_and_is_not_somewhere_outside() { // The two guards it has to be handled before. `Local` says no request is // made at all and this makes one; and `route()` is `None` here too, so the // outside-the-app guard would hand the host an empty address to open. let action = Action::back(); assert!(action.destination.is_back()); assert!(!action.destination.is_local()); assert!(action.destination.route().is_none()); assert_eq!(action.destination.as_str(), ""); } #[test] fn back_from_the_first_screen_goes_nowhere_rather_than_somewhere_wrong() { let mut runtime = Runtime::new(screen_of([Node::Act(Act::new("Close", Action::back()))])); assert_eq!( runtime_press(&mut runtime, &renderer(), "Close"), Step::Idle, "the first screen is not somewhere you arrived at" ); } /// A rule editor's conditions: slots of one repeating question. /// /// audiofiles' shape, cut down, and this is its host. Each condition is a /// region of several fields, which is what `Repeat` could not say. fn conditions(standing: usize, least: usize) -> Screen { let mut group = Slot::new("conditions", RegionKind::Group).repeating( quasi_router::Repeating::new( "Condition", Act::new("Add condition", Action::post("/rules/conditions/add")), ) .least(least), ); for at in 0..standing { group = group.with(Node::Region( Slot::new(format!("condition-{at}"), RegionKind::Group) .with(Node::text(format!("condition {at}"))) .removes(Act::new( "Remove condition", Action::post(format!("/rules/conditions/{at}/remove")), )), )); } Screen::sidebar_content("Rules") .with(Slot::new("main", RegionKind::Pane).with(Node::Region(group))) } #[test] fn the_slots_of_a_repeating_question_are_numbered_for_a_reader() { // `f7abbc08`. One-based, because it is read by a person, and the renderer's // rather than the description's: numbers written into a description go // stale the moment a slot leaves the middle. let mut host = Host::new(); host.settle(&conditions(2, 1)); assert!( on_screen(&host, "Condition 1"), "the slots are not numbered" ); assert!( on_screen(&host, "Condition 2"), "the slots are not numbered" ); assert!( !on_screen(&host, "Condition 0"), "the numbering is zero-based" ); assert!(on_screen(&host, "Add condition"), "nothing adds a slot"); assert!(on_screen(&host, "Remove condition"), "nothing removes one"); } #[test] fn the_floor_stops_the_last_slot_going_rather_than_the_app_doing_it() { // The done condition of the whole member: "at least one condition" is the // description's now, and the last Remove is drawn dead rather than hidden. let alone = conditions(1, 1); let mut host = Host::new(); host.settle(&alone); assert!( on_screen(&host, "Remove condition"), "the boundary hid the control instead of disabling it" ); assert!( host.click(&alone, "Remove condition").is_none(), "the last slot could be removed" ); // One more standing, and it fires. let pair = conditions(2, 1); let mut host = Host::new(); host.settle(&pair); // The second slot's, because `Host::find` takes the last painting of a // label and both controls are called the same thing -- which they are in // the app too. Which one it is matters less than that it carries its own // slot's address: that is `Slot::removes` being the child's and not one // action on the parent with an index bolted to it. assert_eq!( host.click(&pair, "Remove condition") .and_then(|fired| fired.action.route().map(str::to_owned)), Some("/rules/conditions/1/remove".to_owned()), "a slot above the floor could not be removed" ); } #[test] fn a_readers_value_survives_a_fragment_because_the_view_holds_it() { // `a135f898` says the webview has to be told this and that egui was already // right. Confirmed rather than changed: what is typed lives in the `View` // under the field's name, and a fragment replaces a region rather than the // buffer. let field = || Field::new(layout::FieldKind::Text, "tag", "Tag").keeping_value(); let mut runtime = Runtime::new( Screen::sidebar_content("Discover") .with(Slot::new("side", RegionKind::Sidebar).with(Node::field(field()))), ); runtime.view_mut().set("tag", "dru"); runtime.apply( &Request::post("/discover/facet"), Response::from(Outcome::Fragment { region: "side".to_owned(), node: Node::field(field()), }), ); assert_eq!( runtime.view().edit("tag"), Some("dru"), "the reader's value was thrown away by a fragment" ); } #[test] fn an_external_destination_is_handed_back_to_the_host() { let mut runtime = Runtime::new(screen_of([Node::text("first")])); let step = runtime.answer(false); assert!( matches!(step, Step::Idle), "an unasked question does nothing" ); // The question a destructive control raises, answered both ways. let mut runtime = Runtime::new(screen_of([Node::Act( Act::new("Delete", Action::post("/delete")).confirm("Sure?"), )])); assert!(matches!(runtime.answer(true), Step::Idle)); } #[test] fn a_local_action_is_not_an_address_handed_to_the_host() { // `210574ca`, and this is the renderer the ruling was reasoned from: egui // redraws from memory every frame, so a local behaviour is what it already // does and the mark tells it nothing. What it must not do is read "no // route" as "somewhere outside" and hand the host an empty address, which // is what the branch below `send`'s new guard would have done. // // Driven through a chrome binding because that is the one path into `send` // a test can take without a pointer: a key needs no painted rectangle to // land on. let chrome = Chrome::new().bind("ctrl+k", "Dismiss", Action::local()); let mut runtime = Runtime::new(screen_of([Node::text("x")])).with_chrome(chrome); let immediate = renderer(); let ctx = egui::Context::default(); let input = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size( egui::Pos2::ZERO, egui::vec2(900.0, 700.0), )), events: vec![egui::Event::Key { key: egui::Key::K, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::CTRL, }], modifiers: egui::Modifiers::CTRL, ..Default::default() }; let mut step = None; let _ = ctx.run_ui(input, |ui| { step = Some(runtime.show(ui, &immediate)); }); // Not `Step::Open("")`, which is the host being asked to open the empty // address, and not a call: there is no route on a local destination. assert!( matches!(step, Some(Step::Idle)), "a local action produced {step:?} rather than doing nothing" ); // The control, and it is what makes the assertion above mean anything: the // same key on the same harness with an external destination must reach the // host. Without this, a binding that never fired would pass as `Idle`. let chrome = Chrome::new().bind( "ctrl+k", "Docs", Action::external("https://example.invalid"), ); let mut runtime = Runtime::new(screen_of([Node::text("x")])).with_chrome(chrome); let ctx = egui::Context::default(); let input = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size( egui::Pos2::ZERO, egui::vec2(900.0, 700.0), )), events: vec![egui::Event::Key { key: egui::Key::K, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::CTRL, }], modifiers: egui::Modifiers::CTRL, ..Default::default() }; let mut step = None; let _ = ctx.run_ui(input, |ui| { step = Some(runtime.show(ui, &immediate)); }); assert_eq!( step, Some(Step::Open("https://example.invalid".to_string())), "the harness never delivered the key, so the local assertion proved nothing" ); // And the class this renderer declares, which is why ignoring the mark // beyond that is allowed rather than an omission. assert_eq!(crate::CLASS, quasi_router::Renderer::Client); assert!(!crate::CLASS.reads_locality()); } #[test] fn a_chrome_binding_this_renderer_cannot_read_is_ignored_rather_than_guessed() { // The same rule `Act::key` states: the vocabulary of keys is the host's, and // a name this one does not know never matches. let chrome = Chrome::new() .bind("ctrl+k", "Search", Action::get("/palette")) .bind("dpad-left", "Nope", Action::get("/nope")); let runtime = Runtime::new(screen_of([Node::text("x")])).with_chrome(chrome); // Nothing is pressed in a test context, so this asserts the parse rather // than the press: an unreadable name must not panic the frame. let immediate = renderer(); let mut runtime = runtime; egui::__run_test_ui(|ui| { assert!(matches!(runtime.show(ui, &immediate), Step::Idle)); }); } #[test] fn a_method_survives_the_trip_from_description_to_request() { // What a control carries is what the router is asked for. The regression // this guards is a renderer that turns every press into a GET. let immediate = renderer(); let screen = screen_of([Node::Act(Act::new("Delete", Action::post("/delete")))]); let mut view = View::new(); egui::__run_test_ui(|ui| { // Not pressed, so nothing fires; the assertion is that drawing a write // control does not itself produce a request. assert!(immediate.screen(ui, &screen, &mut view).is_none()); }); assert_eq!(Action::post("/delete").method, Method::Post); } #[test] fn a_selection_is_gathered_under_the_name_the_screen_gave_it() { // The commit half of a staged tick: the runtime reads the set the view is // holding and sends it with the call. let mut view = View::new(); view.tick("1"); view.tick("2"); let params = view.gathering(Node::TICKED); let sent: Vec<&str> = params.get_all(Node::TICKED).collect(); assert_eq!(sent, ["1", "2"]); } #[test] fn the_host_can_say_something_no_handler_knows_about() { // A route that failed has to land somewhere the user is looking. For a // windowed app stderr is nowhere. let mut runtime = Runtime::new(screen_of([Node::text("first")])); runtime.say("The library is not reachable."); assert!( runtime.screen().notices.iter().any( |node| matches!(node, Node::Notice { text, .. } if text.contains("not reachable")) ), "the host had nowhere to put it" ); } #[test] fn a_described_table_draws_its_columns_and_cells() { // The node this renderer declined to draw until 2026-08-14. The narrowing // and the tracks are makeover-immediate's; what is asserted here is that a // described table reaches them at all, with a cell holding an ordinary node. use quasi_router::{Cell, Column}; let columns = vec![Column::new("name"), Column::new("bpm")]; let rows = vec![ Row::cells(["kick.wav", "120"]), // A cell holding a control rather than a value, which is what // `CellPart` exists to separate and what a file list actually has. Row::cells([ Cell::new("snare.wav"), Cell::acts([Act::new("Play", Action::post("/play"))]), ]), ]; let mut view = View::new(); let screen = screen_of([Node::Table { marks: ::quasi_router::stage::Marks::none(), columns, rows, more: None, }]); draw(&screen, &mut view); } /// What this file cannot assert, so that the next reader does not spend the /// afternoon finding out. /// /// **The right-click itself is not testable here.** This crate takes egui with /// `default-features = false`, so no font is loaded and every string measures /// zero points wide: a `Response` covers no pixel, nothing is ever hovered, /// and `Response::context_menu` never opens however faithfully the events are /// injected. /// /// So a menu's *gesture* is asserted where a gesture can be: `quasi-tui`, where /// reach is this renderer's own walk and a key press is a value. What is left /// here is that a described menu draws, that it fires nothing unpressed, and /// that the rect it hangs on is this row's rather than the list's -- which is /// the half a font would not have caught either way. /// /// The gesture half is covered now: see "Pressing things" below, which is what /// a dev-only `default_fonts` bought. This one stays because "drawing a menu /// does not open it" is a different claim from "pressing it does". #[test] fn a_row_that_offers_a_menu_draws_and_fires_nothing_unpressed() { use quasi_router::Row; let screen = screen_of([Node::list([ Row::new("kick.wav") .offers(Act::new("Preview", Action::post("/files/1/play"))) .offers( Act::new("Delete", Action::post("/files/1/delete")).confirm("Delete kick.wav?"), ), // And a row beside it that offers nothing, so the interact rect is // claimed for one row and not the other. Row::new("snare.wav"), ])]); let immediate = renderer(); let mut view = View::new(); egui::__run_test_ui(|ui| { assert!( immediate.screen(ui, &screen, &mut view).is_none(), "drawing a menu is not opening one" ); }); } #[test] fn a_table_row_that_offers_a_menu_draws_and_fires_nothing_unpressed() { // The table half, and the one that matters most: audiofiles' file list is a // `Node::Table` and this renderer is what draws it. The collecting slot the // menu uses is separate from the one `activate` uses, so this also covers // the case where a row carries both. use quasi_router::Column; let screen = screen_of([Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: vec![Column::new("Name"), Column::new("BPM")], rows: vec![ Row::cells(["kick.wav", "120"]) .activate(Action::post("/files/1/open")) .offers(Act::new("Preview", Action::post("/files/1/play"))), Row::cells(["snare.wav", "140"]), ], more: None, }]); let immediate = renderer(); let mut view = View::new(); egui::__run_test_ui(|ui| { assert!( immediate.screen(ui, &screen, &mut view).is_none(), "drawing a menu is not opening one" ); }); } #[test] fn a_commit_control_is_inert_until_something_is_ticked() { // The two things a description cannot say about a selection, said here // because this renderer holds the set: how many are in it, and that a // control over none of them should not fire. Drawn rather than hidden, so // the affordance stays on screen and a reader learns bulk actions exist. use quasi_router::Column; let screen = Screen::sidebar_content("Tasks").selecting("chosen").with( Slot::new("main", RegionKind::Pane) .with(Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: vec![Column::new("title")], rows: vec![Row::cells(["First"]).ticking("t-1", false)], more: None, }) .with(Node::Act( Act::new("Complete", Action::post("/tasks/complete")).over("chosen"), )), ); // Nothing ticked: drawing it fires nothing, whatever is clicked. let mut view = View::new(); draw(&screen, &mut view); // With one ticked the control carries the count it would act on. view.tick("t-1"); draw(&screen, &mut view); assert!(view.is_ticked("t-1")); } #[test] fn a_tickable_table_draws_a_column_the_description_did_not_name() { // A tick takes no column in the description -- it does not narrow, sort or // carry data -- and `makeover_immediate::table` addresses cells by column, // so this renderer adds one. The assertion is that the added column does // not disturb the described ones: a two-column table with ticks still // reaches both of its own cells. use quasi_router::Column; let screen = screen_of([Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: vec![Column::new("name"), Column::new("bpm")], rows: vec![ Row::cells(["kick.wav", "120"]).ticking("k", false), Row::cells(["snare.wav", "140"]).ticking("s", true), ], more: None, }]); let mut view = View::new(); draw(&screen, &mut view); } #[test] fn a_table_with_no_columns_draws_nothing_rather_than_panicking() { // `egui_extras` panics on a table with no tracks, and a description with no // columns is reachable. makeover-immediate answers `None` for that case and // this is the assertion that the described path takes it. let mut view = View::new(); let screen = screen_of([Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: Vec::new(), rows: Vec::new(), more: None, }]); draw(&screen, &mut view); } /// One of every shape that could plausibly cache a size. /// /// A field at each [`layout::Width`], a table with mixed /// [`layout::Priority`], a list that says how much more there is, and a nested /// region. Written out here rather than shared with the other two renderers' /// copies: the fixture is a few lines, and sharing it would mean a new public /// surface on a crate for the sake of a test. fn every_shape_that_could_cache() -> Screen { Screen::sidebar_content("Any width").with( Slot::new("main", RegionKind::Pane) .with(Node::Field(Box::new(Field::new( layout::FieldKind::Text, "wide", "Wide", )))) .with(Node::Field(Box::new( Field::new(layout::FieldKind::Text, "tight", "Tight").width(layout::Width::Content), ))) .with(Node::Field(Box::new( Field::new(layout::FieldKind::Text, "held", "Held").width(layout::Width::Fixed), ))) .with(Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: vec![ quasi_router::Column::new("Name").priority(layout::Priority::Essential), quasi_router::Column::new("Kind").priority(layout::Priority::Secondary), quasi_router::Column::new("Added").priority(layout::Priority::Optional), ], rows: vec![quasi_router::Row::cells([ "kick.wav", "sample", "2026-08-12", ])], more: Some(quasi_router::Rest::more(1, Action::get("/samples?from=1"))), }) .with(Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: Vec::new(), rows: vec![quasi_router::Row::new("one"), quasi_router::Row::new("two")], more: Some(quasi_router::Rest::more(2, Action::get("/rows?from=2"))), }) .with(Node::Region( Slot::group("nested").with(Node::text("inside")), )), ) } /// Everything the renderer put on the screen at this width, as text. /// /// Through a real `Context` rather than `__run_test_ui`, because the width is /// the whole subject here and the test helper picks its own. The shapes are /// compared by their debug form: what is being asserted is that two frames are /// the same picture, and equality of the primitives is the strongest available /// statement of that. fn painted( ctx: &egui::Context, immediate: &Immediate, screen: &Screen, view: &mut View, width: f32, ) -> String { let input = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size( egui::Pos2::ZERO, egui::vec2(width, 900.0), )), ..Default::default() }; let output = ctx.run_ui(input, |ctx| { egui::CentralPanel::default().show(ctx, |ui| { immediate.screen(ui, screen, view); }); }); format!("{:?}", output.shapes) } #[test] fn a_run_is_capped_at_the_lines_its_flow_allows() { use quasi_router::Row; // The cap, settled by `7bfb554a`. Before it a long part in a row grew to as // many rows as the words needed, which is a block's behaviour in a place // that is a line. let long = "a headline long enough that it certainly will not fit across a narrow pane"; let ctx = egui::Context::default(); let immediate = renderer(); let tight = painted( &ctx, &immediate, &screen_of([Node::list([Row::new(long)])]), &mut View::new(), 200.0, ); let relaxed = painted( &ctx, &immediate, &screen_of([Node::list([Row::new(long).relaxed()])]), &mut View::new(), 200.0, ); // Elided in both, because a cap is not a promise of room, and the ellipsis // is what tells a reader the rest is there. assert!(tight.contains('\u{2026}'), "{tight}"); assert!(relaxed.contains('\u{2026}'), "{relaxed}"); // Two rows carry more of the headline than one, and the picture says so. assert!(relaxed.len() > tight.len(), "{relaxed}"); } #[test] fn a_long_part_leaves_room_for_what_follows_it() { use quasi_router::{Row, Tag}; // The defect the budget fixes. `capped` gave the first text leaf the whole // available width, so the badge and the count after it started past the // right edge and egui clipped them: gone with no ellipsis and no sign they // had ever been there. let long = "a headline long enough that it certainly will not fit across this pane"; let ctx = egui::Context::default(); let immediate = renderer(); let out = painted( &ctx, &immediate, &screen_of([Node::list([Row::new(long) .token(Tag::badge("beta")) .meta("2 files")])]), &mut View::new(), 320.0, ); assert!(out.contains("beta"), "{out}"); assert!(out.contains("2 files"), "{out}"); // The headline is what gave way, which is the trade: it elides, they do not // vanish. assert!(out.contains('\u{2026}'), "{out}"); } #[test] fn a_budget_that_cannot_fit_the_tail_is_not_taken() { use quasi_router::Row; // quasi-tui's rule from the other end. If the reservation is wider than the // pane, eliding the headline buys room for parts that are still past the // edge, so the headline keeps the width and the tail is lost either way. let long = "a headline long enough that it certainly will not fit across this pane"; let ctx = egui::Context::default(); let immediate = renderer(); let narrow = painted( &ctx, &immediate, &screen_of([Node::list([ Row::new(long).meta("a trailing fact that is itself far too wide for the pane") ])]), &mut View::new(), 120.0, ); let alone = painted( &ctx, &immediate, &screen_of([Node::list([Row::new(long)])]), &mut View::new(), 120.0, ); // The headline is laid out the same either way: the tail took nothing from // it, because there was nothing to take that would have helped. assert!(narrow.contains(&headline_of(&alone)), "{narrow}"); } /// The first galley in a painted frame, as its debug form. /// /// Comparing whole frames would compare the tail too, and the tail is the thing /// that differs; what is being asserted is that the *headline* was laid out the /// same, which is the part a budget would have changed. fn headline_of(painted: &str) -> String { let start = painted.find("Galley").expect("a galley"); painted[start..start + 120].to_owned() } #[test] fn a_frame_is_the_same_picture_however_the_window_got_here() { // "Any width, one answer", `makeover-layout` 0.27.4. The same description // at the same width is the same frame, whatever widths came before it. // // The renderer, the context and the view are made once and reused across // the sequence, which is the half that matters: a fresh `Immediate` per // frame could not fail this test however much geometry it kept. egui makes // the property easy to lose rather than easy to keep -- an immediate-mode // library hands you `ui.available_width()` every frame and a memory store // to put the answer in -- so this is the renderer where the guard earns // its place. let screen = every_shape_that_could_cache(); let ctx = egui::Context::default(); let immediate = renderer(); let mut view = View::new(); let cold = painted(&ctx, &immediate, &screen, &mut view, 400.0); for width in [1200.0, 320.0, 900.0, 200.0] { let _ = painted(&ctx, &immediate, &screen, &mut view, width); } assert_eq!(painted(&ctx, &immediate, &screen, &mut view, 400.0), cold); // And the fixture is one the width actually moves, or the assertion above // would be true of a blank frame. assert_ne!(painted(&ctx, &immediate, &screen, &mut view, 1200.0), cold); } #[test] fn a_region_narrows_by_dropping_the_members_that_said_they_could_go() { // The same declared rule the other two renderers apply, in the third // renderer's units. The assertion is over what was painted rather than // over the cutoff helper alone, because the helper being right and the // walk ignoring it is the failure worth catching. let screen = Screen::sidebar_content("Toolbar").with( Slot::new("bar", RegionKind::Band) .with(Node::text("Library")) .with_ranked(Node::text("Filter"), layout::Priority::Secondary) .with_ranked(Node::text("Sort"), layout::Priority::Optional), ); let ctx = egui::Context::default(); let immediate = renderer(); let mut view = View::new(); let wide = painted(&ctx, &immediate, &screen, &mut view, 1000.0); let middling = painted(&ctx, &immediate, &screen, &mut view, 700.0); let narrow = painted(&ctx, &immediate, &screen, &mut view, 400.0); // Fewer shapes each step down, and never more: the cutoff only rises. assert!(middling.len() < wide.len(), "{middling}"); assert!(narrow.len() < middling.len(), "{narrow}"); // And the width alone decides it, so coming back is going back. assert_eq!(painted(&ctx, &immediate, &screen, &mut view, 1000.0), wide); } #[test] fn the_cutoff_boundaries_are_the_size_classes_and_not_this_renderers_taste() { // 600 and 840 are `makeover-geometry`'s, quoted from Material's window // size classes and used verbatim by the webview's `@media` rules. Held // here as a test rather than as a comment because the cost of drifting off // them is invisible: two hosts showing one screen would narrow at // different widths and neither would look wrong on its own. assert_eq!(crate::node::cutoff(599.0), layout::Priority::Essential); assert_eq!(crate::node::cutoff(600.0), layout::Priority::Secondary); assert_eq!(crate::node::cutoff(839.0), layout::Priority::Secondary); assert_eq!(crate::node::cutoff(840.0), layout::Priority::Optional); } #[test] fn each_question_about_one_box_keeps_its_own_deadline() { // `N8`. One deadline per field would have the faster of two questions // cancel the slower, which is the cost the ruling named for this renderer // and the whole of what the second half of the key buys. let mut view = View::new(); let now = std::time::Instant::now(); let box_q = || Asking::Field("q".to_owned()); view.wait_to_consult(box_q(), 0, now + std::time::Duration::from_millis(200)); view.wait_to_consult(box_q(), 1, now + std::time::Duration::from_millis(150)); assert_eq!( view.consult_due(box_q(), 0), Some(now + std::time::Duration::from_millis(200)) ); assert_eq!( view.consult_due(box_q(), 1), Some(now + std::time::Duration::from_millis(150)) ); // Asking one leaves the other waiting. view.consulted(box_q(), 1); assert!(view.consult_due(box_q(), 0).is_some()); assert_eq!(view.consult_due(box_q(), 1), None); // `cb62a9dc`. A region that shares the box's name is a different asker, so // its own deadline is untouched by either call above. let region_q = || Asking::Region("q".to_owned()); view.wait_to_consult(region_q(), 1, now + std::time::Duration::from_millis(300)); assert_eq!(view.consult_due(box_q(), 1), None); assert_eq!( view.consult_due(region_q(), 1), Some(now + std::time::Duration::from_millis(300)) ); } #[test] fn a_regions_question_comes_due_and_carries_every_dial_inside_it() { // `cb62a9dc`. The wait is the view's and the gathering is the drawing's, so // a deadline already past is what a frame needs to fire one. What is // asserted is the payload: every dial the region holds, at every depth, // and the untouched ones sending what they are showing. let mut view = View::new(); view.wait_to_consult( Asking::Region("calculator".to_owned()), 0, std::time::Instant::now() .checked_sub(std::time::Duration::from_millis(1)) .expect("the clock has run for a millisecond"), ); view.set("sales", "40"); let screen = Screen::sidebar_content("Pricing").with( Slot::group("calculator") .with(Node::field( Field::new(layout::FieldKind::Number, "item_price", "Price").value("10"), )) .with(Node::field(Field::new( layout::FieldKind::Number, "sales", "Sales", ))) .with(Node::Region(Slot::group("other").with(Node::field( Field::new(layout::FieldKind::Number, "other_pct", "Their cut").value("30"), )))) .consulting(Consult::new( Action::get("/pricing/compare").replacing("results-panel"), )), ); let immediate = renderer(); let mut fired = None; egui::__run_test_ui(|ui| { fired = immediate.screen(ui, &screen, &mut view); }); let fired = fired.expect("a deadline that has passed is a question asked"); assert_eq!(fired.action.destination.route(), Some("/pricing/compare")); assert_eq!(fired.payload.get("sales"), Some("40")); assert_eq!(fired.payload.get("item_price"), Some("10")); assert_eq!(fired.payload.get("other_pct"), Some("30")); // Asked once. The wait is gone, so the next frame does not ask again. assert_eq!( view.consult_due(Asking::Region("calculator".to_owned()), 0), None ); } #[test] fn a_question_carries_the_controls_it_says_it_carries() { // Discover's results route answers about the current filters. Read out of // what has been drawn, which is where a described field's own offer lands // the first time it is drawn, so an untouched filter still contributes. let mut view = View::new(); view.set("mode", "mine"); let carried = view.contributed(&["mode".to_owned(), "absent".to_owned()]); assert_eq!(carried.get("mode"), Some("mine")); // A name nothing on the screen carries contributes nothing rather than an // empty value, so a route can tell "not on this screen" from "on it and // blank". assert_eq!(carried.get("absent"), None); } // ── Pressing things ── // // `egui::__run_test_ui` runs one frame with no input, which is enough to assert // what a description drew and nothing about what pressing it does. A context // menu opens on one frame and is pressed on a later one, so an interaction test // needs the `Context` kept between frames and the pointer told where it is. // // The other half is fonts. With `default-features = false` every string measures // zero points wide, so a `Response` covers no pixel, is never hovered, and no // injected click reaches it however faithfully it is sent. The dev-dependency in // `Cargo.toml` is what makes the rest of this section possible; see the note // there for why it is dev-only. // // Positions come from the shapes the previous frame painted rather than from // arithmetic over the layout. A test that computed "the row is 18 points down" // would assert the theme's metrics as much as the renderer's wiring, and would // have to be rewritten whenever a margin changed. /// A real context, one frame at a time. struct Host { ctx: egui::Context, view: View, painted: Vec, /// What is being held down while the next press lands. /// /// On the host rather than passed to `click`, because it has to reach two /// places that do not share a call: the `PointerButton` event and /// `RawInput::modifiers`, which is where `Context::input().modifiers` comes /// from and therefore what a renderer reading the keyboard sees. held: egui::Modifiers, } impl Host { fn new() -> Self { Self { ctx: egui::Context::default(), view: View::new(), painted: Vec::new(), held: egui::Modifiers::default(), } } /// Draw `screen` once with `events` delivered, and return what it fired. fn frame(&mut self, screen: &Screen, events: Vec) -> Option { let input = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size( egui::Pos2::ZERO, egui::vec2(900.0, 700.0), )), events, modifiers: self.held, ..Default::default() }; let immediate = renderer(); let mut fired = None; let output = self.ctx.run_ui(input, |ui| { fired = immediate.screen(ui, screen, &mut self.view); }); self.painted = output.shapes; fired } /// Draw until the layout settles, discarding what it fires. /// /// egui needs a pass to learn a widget's size before it can answer a /// pointer over it, and a menu adds another for its own area. fn settle(&mut self, screen: &Screen) { for _ in 0..3 { self.frame(screen, Vec::new()); } } /// The middle of the last frame's rendering of `text`. /// /// Panics rather than returning an option: every caller is asserting that /// something is on screen, and "not painted" is a failure with a much better /// message here than a later `None` unwrap at the press. fn find(&self, text: &str) -> egui::Pos2 { fn walk(shape: &egui::Shape, text: &str, found: &mut Option) { match shape { egui::Shape::Text(t) if t.galley.job.text.contains(text) => { *found = Some(egui::Rect::from_min_size(t.pos, t.galley.size())); } egui::Shape::Vec(shapes) => { for shape in shapes { walk(shape, text, found); } } _ => {} } } let mut found = None; for clipped in &self.painted { walk(&clipped.shape, text, &mut found); } let rect = found.unwrap_or_else(|| { panic!("nothing painted the text {text:?}; the press has nowhere to land") }); assert!( rect.width() > 0.0, "{text:?} painted zero points wide, so nothing can be pressed on it" ); rect.center() } /// Move the pointer to `pos` and press and release `button` there. /// /// Three frames because that is what egui takes: one to notice the pointer, /// one for the press, one for the release. The fired action can come back on /// any of them, so the first that answers wins. fn click_at( &mut self, screen: &Screen, pos: egui::Pos2, button: egui::PointerButton, ) -> Option { let modifiers = self.held; let press = |pressed| egui::Event::PointerButton { pos, button, pressed, modifiers, }; let frames = [ vec![egui::Event::PointerMoved(pos)], vec![egui::Event::PointerMoved(pos), press(true)], vec![egui::Event::PointerMoved(pos), press(false)], vec![egui::Event::PointerMoved(pos)], ]; let mut fired = None; for events in frames { fired = self.frame(screen, events).or(fired); } fired } /// Click `text` where it was last painted. fn click(&mut self, screen: &Screen, text: &str) -> Option { let pos = self.find(text); self.click_at(screen, pos, egui::PointerButton::Primary) } /// Click `text` with these keys held. fn click_holding( &mut self, screen: &Screen, text: &str, held: egui::Modifiers, ) -> Option { self.held = held; let fired = self.click(screen, text); self.held = egui::Modifiers::default(); fired } /// Right-click `text` where it was last painted. fn right_click(&mut self, screen: &Screen, text: &str) -> Option { let pos = self.find(text); self.click_at(screen, pos, egui::PointerButton::Secondary) } } /// A screen with a band, a pane and a band, said in that order. fn topped_and_tailed() -> Screen { Screen::sidebar_content("Test") .with(Slot::new("bar", RegionKind::Band).with(Node::text("the toolbar"))) .with(Slot::new("main", RegionKind::Pane).with(Node::text("the content"))) .with(Slot::new("foot", RegionKind::Band).with(Node::text("the status"))) } /// An outline: `drums` shut over a child, `genre` open over one. fn outline() -> Screen { screen_of([Node::list([ quasi_router::Row::new("drums") .disclosing(false) .activate(Action::get("/tags/drums")), quasi_router::Row::new("drums.kick") .depth(quasi_router::layout::Nesting::at(1)) .activate(Action::get("/k")), quasi_router::Row::new("genre").disclosing(true), quasi_router::Row::new("genre.house").depth(quasi_router::layout::Nesting::at(1)), ])]) } /// Whether anything painted this text on the last frame. fn on_screen(host: &Host, text: &str) -> bool { fn walk(shape: &egui::Shape, text: &str) -> bool { match shape { egui::Shape::Text(t) => t.galley.job.text.contains(text), egui::Shape::Vec(shapes) => shapes.iter().any(|shape| walk(shape, text)), _ => false, } } host.painted .iter() .any(|clipped| walk(&clipped.shape, text)) } #[test] fn a_shut_branch_draws_neither_its_children_nor_asks_the_app_anything() { // `ccaa7e4b`. The rows are in the description either way; which of them are // on screen is `Row::open`'s answer, and pressing the chevron is the reader // tidying their own view rather than a write. let mut host = Host::new(); let screen = outline(); host.settle(&screen); assert!(on_screen(&host, "drums"), "the branch is drawn"); assert!( !on_screen(&host, "drums.kick"), "a shut branch hides its own" ); assert!(on_screen(&host, "genre.house"), "an open one does not"); // The chevron opens it, and calls no route. let fired = host.click(&screen, "\u{25b6}"); assert!(fired.is_none(), "folding asked the app for something"); host.settle(&screen); assert!(on_screen(&host, "drums.kick"), "the branch did not open"); } #[test] fn pressing_a_branchs_chevron_is_not_pressing_the_branch() { // A separate hit target from the label, which is the shipped egui sidebar's // own behaviour: pressing a tag filters by it and pressing its chevron does // not. The row's own route still answers a press on its words. let mut host = Host::new(); let screen = outline(); host.settle(&screen); let fired = host.click(&screen, "drums"); assert_eq!( fired.and_then(|fired| fired.action.route().map(str::to_string)), Some("/tags/drums".to_string()) ); } #[test] fn a_band_said_after_the_body_is_drawn_under_it() { // Ruled by Max 2026-08-23 (quasicoherent 3725bacf): where a band was said // is which end it belongs to. Before it this renderer hoisted every band, // so a footer drew above the content it was the footer of while the same // description put it underneath in a webview. let mut host = Host::new(); let screen = topped_and_tailed(); host.settle(&screen); let bar = host.find("the toolbar"); let content = host.find("the content"); let foot = host.find("the status"); assert!( bar.y < content.y, "the toolbar left the top: {bar:?} {content:?}" ); assert!( content.y < foot.y, "the footer is above what it is the footer of: {content:?} {foot:?}" ); } #[test] fn the_band_said_last_is_the_one_at_the_bottom() { // Two trailing bands, which is audiofiles' shell: a migration strip above // the status band. A bottom-up layout puts the first widget lowest, so the // order they are drawn in is the reverse of the order they were said in, // and getting that wrong is invisible until there are two. let screen = Screen::sidebar_content("Test") .with(Slot::new("main", RegionKind::Pane).with(Node::text("the content"))) .with(Slot::new("strip", RegionKind::Band).with(Node::text("the strip"))) .with(Slot::new("foot", RegionKind::Band).with(Node::text("the status"))); let mut host = Host::new(); host.settle(&screen); let strip = host.find("the strip"); let foot = host.find("the status"); assert!( strip.y < foot.y, "the bands are upside down: {strip:?} {foot:?}" ); } #[test] fn a_screen_of_nothing_but_bands_is_unchanged() { // The shape every description written before the ruling had. With no body // to be after, every band is a leading one. let screen = Screen::sidebar_content("Test") .with(Slot::new("one", RegionKind::Band).with(Node::text("first"))) .with(Slot::new("two", RegionKind::Band).with(Node::text("second"))); let mut host = Host::new(); host.settle(&screen); assert!(host.find("first").y < host.find("second").y); } /// A band whose members were said to share one row. fn run_of(fallback: layout::Fallback, members: [(&str, layout::Priority); 3]) -> Screen { let mut row = Run::new(fallback); for (label, priority) in members { row = row.beside( Node::Act(Act::new(label, Action::post(format!("/{label}")))), priority, ); } Screen::sidebar_content("Test").with(Slot::new("bar", RegionKind::Band).across(row)) } #[test] fn a_member_of_a_run_is_drawn_at_all() { // The defect: `region` walked `body` and nothing walked `run`, so a // description that said `across` and then `part` contributed controls that // were never painted and could never be pressed. `Row::menu`'s bug of // 2026-08-17 in a second place, and silent in the same way. let screen = run_of( layout::Fallback::Wrap, [ ("Import", layout::Priority::Essential), ("Export", layout::Priority::Essential), ("Settings", layout::Priority::Essential), ], ); let mut host = Host::new(); host.settle(&screen); assert_eq!( host.click(&screen, "Export").map(|fired| fired.action), Some(Action::post("/Export")), "a described control in a row must send what it said it sends" ); } #[test] fn a_run_puts_its_members_across_rather_than_down() { // What the row is for. Three controls in a band drew one per line before // this, which is the shape a toolbar is not. let screen = run_of( layout::Fallback::Wrap, [ ("Import", layout::Priority::Essential), ("Export", layout::Priority::Essential), ("Settings", layout::Priority::Essential), ], ); let mut host = Host::new(); host.settle(&screen); let first = host.find("Import"); let second = host.find("Export"); let third = host.find("Settings"); assert!( (first.y - second.y).abs() < 1.0 && (second.y - third.y).abs() < 1.0, "the members left the row: {first:?} {second:?} {third:?}" ); assert!( first.x < second.x && second.x < third.x, "the members are out of the order they were said in: {first:?} {second:?} {third:?}" ); } #[test] fn a_run_that_sheds_keeps_what_the_description_called_essential() { // The half a webview cannot do at all: there is no // `@container (inline-size < min-content)`, so `Shed` wraps there. This // renderer is holding the width, so it can honour the word. let screen = run_of( layout::Fallback::Shed, [ ("Import", layout::Priority::Essential), ("Export", layout::Priority::Secondary), ("Settings", layout::Priority::Optional), ], ); let ctx = egui::Context::default(); let immediate = renderer(); let narrow = painted(&ctx, &immediate, &screen, &mut View::new(), 500.0); assert!(narrow.contains("Import"), "the essential member was shed"); assert!( !narrow.contains("Export") && !narrow.contains("Settings"), "a shed row kept what it said it would drop: {narrow}" ); // And nothing is dropped when there is room for all three, or the cutoff // would be a permanent narrowing rather than a measurement. let wide = painted(&ctx, &immediate, &screen, &mut View::new(), 1000.0); assert!( wide.contains("Export") && wide.contains("Settings"), "{wide}" ); } #[test] fn a_run_that_menus_keeps_every_member_reachable() { // `Shed` and `Menu` drop the same members. What separates them is where the // dropped ones go, and a `Menu` that dropped them on the floor would be // this renderer answering with `Shed`. let screen = run_of( layout::Fallback::Menu, [ ("Import", layout::Priority::Essential), ("Export", layout::Priority::Secondary), ("Settings", layout::Priority::Optional), ], ); let ctx = egui::Context::default(); let immediate = renderer(); let narrow = painted(&ctx, &immediate, &screen, &mut View::new(), 500.0); assert!(narrow.contains("Import")); assert!( narrow.contains("2 more"), "the two shed members went nowhere a reader could follow: {narrow}" ); } #[test] fn a_region_with_no_run_draws_exactly_what_it_did_before() { // The change is additive at the call site, so a description written before // runs existed must paint the same picture. let screen = screen_of([Node::Act(Act::new("Save", Action::post("/save")))]); let ctx = egui::Context::default(); let immediate = renderer(); let before = painted(&ctx, &immediate, &screen, &mut View::new(), 900.0); assert!(before.contains("Save")); assert!( !before.contains("more"), "a region with no run grew a control out of nothing: {before}" ); } /// A list of two rows, the first offering both an opening route and a menu. fn menu_list() -> Screen { use quasi_router::Row; screen_of([Node::list([ Row::new("kick.wav") .activate(Action::get("/files/1")) .offers(Act::new("Preview", Action::post("/files/1/play"))) .offers( Act::new("Delete", Action::post("/files/1/delete")).confirm("Delete kick.wav?"), ), // A row beside it offering nothing, so an interact rect claimed for the // wrong row shows up as the wrong route rather than as no route. Row::new("snare.wav").activate(Action::get("/files/2")), ])]) } #[test] fn a_control_over_the_selection_refuses_an_empty_set_and_sends_the_ticks() { // `Act::over` reached this renderer and did nothing until 2026-08-20: the // member was read into a parameter both callers passed `None` for, so the // count went undrawn, the press over nothing went through, and what it sent // travelled under the selection's name rather than under `Node::TICKED`. use quasi_router::Column; let screen = Screen::sidebar_content("Tasks").selecting("chosen").with( Slot::new("main", RegionKind::Pane) .with(Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: vec![Column::new("title")], rows: vec![Row::cells(["First"]).ticking("t-1", false)], more: None, }) .with(Node::Act( Act::new("Complete", Action::post("/tasks/complete")).over("chosen"), )), ); let mut host = Host::new(); host.settle(&screen); assert!( host.click(&screen, "Complete").is_none(), "a press over an empty selection is refused" ); host.view.tick("t-1"); host.settle(&screen); let fired = host .click(&screen, "Complete") .expect("the control answers"); assert_eq!( fired.payload.get_all(Node::TICKED).collect::>(), ["t-1"] ); } #[test] fn clicking_a_control_that_asked_for_a_value_sends_it_with_the_ticks() { // `033ff3ca`. The box stands beside the control here rather than behind a // disclosure, so what is asserted is the payload: the value the box holds // and the set the verb acts over, in one call. use quasi_router::{Column, Field}; let screen = Screen::sidebar_content("Items").selecting("chosen").with( Slot::new("main", RegionKind::Pane) .with(Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: vec![Column::new("title")], rows: vec![Row::cells(["First"]).ticking("i-1", false)], more: None, }) .with(Node::Act( Act::new("Set Price", Action::post("/items/price")) .over("chosen") .asking(Field::new( layout::FieldKind::Number, "price", "New price ($)", )), )), ); let mut host = Host::new(); host.settle(&screen); // What the reader would have typed and ticked. Typing into the box through // events is egui's own text edit rather than anything described here. host.view.set("price", "12"); host.view.tick("i-1"); host.settle(&screen); let fired = host.click(&screen, "Set Price").expect("the verb answers"); assert_eq!(fired.action, Action::post("/items/price")); assert_eq!(fired.payload.get("price"), Some("12")); assert_eq!( fired.payload.get_all(Node::TICKED).collect::>(), ["i-1"] ); } #[test] fn clicking_a_row_opens_it() { let screen = menu_list(); let mut host = Host::new(); host.settle(&screen); let fired = host.click(&screen, "kick.wav").expect("the row answers"); assert_eq!(fired.action, Action::get("/files/1")); } #[test] fn each_row_answers_for_itself() { // The `ui.min_rect()` defect: every row of a list draws into one shared // `Ui`, so a rect taken off the `Ui` grew with each row and the later rows' // targets covered the earlier ones. Clicking the second row and getting the // first row's route is exactly what that looked like. let screen = menu_list(); let mut host = Host::new(); host.settle(&screen); let fired = host.click(&screen, "snare.wav").expect("the row answers"); assert_eq!(fired.action, Action::get("/files/2")); } #[test] fn right_clicking_a_row_opens_its_menu_and_the_menu_fires() { let screen = menu_list(); let mut host = Host::new(); host.settle(&screen); assert!( host.right_click(&screen, "kick.wav").is_none(), "asking what a row offers is not opening it" ); let fired = host .click(&screen, "Preview") .expect("the menu item answers"); assert_eq!(fired.action, Action::post("/files/1/play")); assert_eq!(fired.confirm, None); } #[test] fn a_menu_item_carries_the_confirmation_it_was_described_with() { let screen = menu_list(); let mut host = Host::new(); host.settle(&screen); host.right_click(&screen, "kick.wav"); let fired = host .click(&screen, "Delete") .expect("the menu item answers"); assert_eq!(fired.action, Action::post("/files/1/delete")); assert_eq!(fired.confirm.as_deref(), Some("Delete kick.wav?")); } #[test] fn a_table_row_answers_the_menu_on_whichever_cell_was_pressed() { // The table half, and the one that matters most: audiofiles' file list is a // `Node::Table`. `makeover_immediate::table` answers a `Ui` per cell and no // row-wide rect, so the menu hangs off every cell of the row, and the // second column is what proves it rather than the first. use quasi_router::Column; let screen = screen_of([Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: vec![Column::new("Name"), Column::new("BPM")], rows: vec![ Row::cells(["kick.wav", "120"]) .activate(Action::post("/files/1/open")) .offers(Act::new("Preview", Action::post("/files/1/play"))), Row::cells(["snare.wav", "140"]).activate(Action::post("/files/2/open")), ], more: None, }]); let mut host = Host::new(); host.settle(&screen); assert!( host.right_click(&screen, "120").is_none(), "asking what a row offers is not opening it" ); let fired = host .click(&screen, "Preview") .expect("the menu item answers"); assert_eq!(fired.action, Action::post("/files/1/play")); } #[test] fn a_table_row_without_a_menu_still_opens() { use quasi_router::Column; let screen = screen_of([Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: vec![Column::new("Name")], rows: vec![ Row::cells(["kick.wav"]).activate(Action::post("/files/1/open")), Row::cells(["snare.wav"]).activate(Action::post("/files/2/open")), ], more: None, }]); let mut host = Host::new(); host.settle(&screen); let fired = host.click(&screen, "snare.wav").expect("the row answers"); assert_eq!(fired.action, Action::post("/files/2/open")); } #[test] fn a_region_fed_by_a_call_is_asked_for_and_then_stops_asking() { // `d8d6f380`. egui has no browser under it either, so the host performs the // region's call the way it performs every other one. let mut runtime = Runtime::new(Screen::sidebar_content("Payments").with( Slot::new("payouts", RegionKind::Pane).fed_by(Action::get("/dashboard/payouts").awaiting()), )); let feeds = runtime.feeds(); assert_eq!(feeds.len(), 1); assert_eq!(feeds[0].path, "/dashboard/payouts"); assert_eq!(feeds[0].method, Method::Get); runtime.apply( &feeds[0].clone(), Response { outcome: Outcome::Fragment { region: "payouts".to_owned(), node: Node::text("$12.00"), }, notice: None, address: None, invalidates: Vec::new(), }, ); assert!( runtime.feeds().is_empty(), "a region that has been filled asks again" ); } #[test] fn a_live_region_is_re_asked_on_the_cadence_and_not_faster() { // The half `feeds` deliberately does not do. A feed is cleared as it lands // and a cadence is not, so the pacing has to live somewhere, and it lives // here rather than in every host that draws a live screen. let mut runtime = Runtime::new( Screen::sidebar_content("Admin").with( Slot::new("queue", RegionKind::Pane) .fed_by(Action::get("/admin/queue")) .live(), ), ); // A live call is never a feed, so a host performing feeds asks for nothing. assert!(runtime.feeds().is_empty()); assert!(runtime.is_live()); let start = std::time::Instant::now(); let first = runtime.refreshes_at(start); assert_eq!(first.len(), 1); assert_eq!(first[0].path, "/admin/queue"); // Asked again a frame later, which is what an event loop does: nothing, or // the rate would be the loop's rather than this crate's. assert!( runtime .refreshes_at(start + std::time::Duration::from_millis(16)) .is_empty() ); // The answer landing leaves the region live, which is what `replace` // learned, so the next period asks again. runtime.apply( &first[0].clone(), Response::fragment("queue", Node::text("4 waiting")), ); assert_eq!(runtime.refreshes_at(start + crate::CADENCE).len(), 1); } /// The write is offloaded, the route answers that it started, and the region's /// own live call is what reports the finish. This host draws the wait off the /// readiness axis, so nothing here has to look at the sentence. #[test] fn work_handed_off_leaves_the_region_waiting_and_keeps_the_call_that_reports_it() { let mut runtime = Runtime::new( Screen::sidebar_content("Import & Export").with( Slot::new("backups", RegionKind::Pane) .fed_by(Action::get("/backups")) .live(), ), ); let start = std::time::Instant::now(); let first = runtime.refreshes_at(start); runtime.apply( &first[0].clone(), Response::fragment("backups", Node::text("3 backups")), ); let region = |runtime: &Runtime| { runtime .screen() .slots .iter() .find_map(|slot| slot.find("backups")) .expect("the region is on the screen") .clone() }; assert_eq!(region(&runtime).readiness, layout::Readiness::Ready); runtime.apply( &Request::post("/backups/create"), Response::started("backups", "Creating backup…"), ); let waiting = region(&runtime); assert_eq!(waiting.readiness, layout::Readiness::Pending); assert_eq!( waiting.body.get(0).expect("a member").node, Node::pending("Creating backup…") ); // Still the same screen: handing work off is not a navigation and puts up // no layer. assert_eq!(runtime.screen().title, "Import & Export"); // And the cadence survived, which is what makes the finish reportable. assert_eq!(runtime.refreshes_at(start + crate::CADENCE).len(), 1); } #[test] fn a_still_screen_refreshes_nothing() { let mut runtime = Runtime::new( Screen::sidebar_content("Payments") .with(Slot::new("payouts", RegionKind::Pane).fed_by(Action::get("/dashboard/payouts"))), ); assert!(!runtime.is_live()); assert!(runtime.refreshes().is_empty()); assert_eq!(runtime.feeds().len(), 1, "a still region is still a feed"); } #[test] fn a_live_region_with_no_call_re_asks_the_screens_own_address() { // The audiofiles sync panel: state the host already holds, moved by an // OAuth callback landing in another process. There is no fragment to fetch, // so re-reading it is building the description again, which is the address // the screen came from. let mut runtime = Runtime::new( Screen::sidebar_content("Sync").with( Slot::new("sync", RegionKind::Pane) .live() .with(Node::text("Authenticating")), ), ); assert!(runtime.is_live()); // Nothing to ask until the runtime knows where the screen came from, which // is what `apply` records. A first screen handed straight to `new` has no // address, and inventing one would be a route this crate made up. assert!(runtime.refreshes().is_empty()); let home = Request::get("/sync"); runtime.apply( &home, Response::from( Screen::sidebar_content("Sync").with( Slot::new("sync", RegionKind::Pane) .live() .with(Node::text("Needs encryption")), ), ), ); let due = runtime.refreshes_at(std::time::Instant::now() + crate::CADENCE); assert_eq!(due.len(), 1); assert_eq!(due[0].path, "/sync"); } #[test] fn the_control_being_waited_on_is_the_one_that_is_busy() { // What `act_node` reads to disable the control that was pressed, and the // whole guard with it: a disabled widget reports no click. let mut view = View::new(); let buying = Action::post("/checkout").awaiting(); assert!(!view.busy(&buying)); view.await_on(Some(buying.clone())); assert!(view.busy(&buying)); // One control, not the app: everything else on the screen still answers. assert!(!view.busy(&Action::post("/cancel"))); // A screen arriving is the answer, or is somewhere else entirely. view.reset(); assert!(!view.busy(&buying)); } #[test] fn a_screen_waiting_on_a_control_still_draws() { // One frame with an outstanding control, which is the state every frame // between the press and the answer is in. let screen = screen_of([Node::Act(Act::new( "Buy", Action::post("/checkout").awaiting(), ))]); let mut view = View::new(); view.await_on(Some(Action::post("/checkout").awaiting())); draw(&screen, &mut view); } // ── The frame a mount puts around a screen ── // // Through a real `Context` for `painted`'s reason: what is asserted is the // order things were laid out in, and that is only readable off the shapes. /// One frame's painted shapes, as their debug form. fn framed_shapes( ctx: &egui::Context, immediate: &Immediate, screen: &Screen, frame: &Frame, view: &mut View, ) -> String { let input = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size( egui::Pos2::ZERO, egui::vec2(600.0, 900.0), )), ..Default::default() }; let output = ctx.run_ui(input, |ctx| { egui::CentralPanel::default().show(ctx, |ui| { immediate.framed(ui, screen, frame, view); }); }); format!("{:?}", output.shapes) } /// Where in the painting a piece of text first appears. /// /// A character offset into the shapes' debug form, which is enough to order two /// things against each other and is what every assertion here needs. fn painted_at(shapes: &str, text: &str) -> usize { shapes .find(text) .unwrap_or_else(|| panic!("{text:?} was never painted")) } #[test] fn a_mount_that_declares_no_frame_draws_what_it_always_drew() { // The default has to be the old picture, or every host that puts a screen // up changes what it paints when this arrives. let ctx = egui::Context::default(); let immediate = renderer(); let screen = screen_of([Node::text("body")]); let framed = framed_shapes(&ctx, &immediate, &screen, &Frame::new(), &mut View::new()); let plain = painted(&ctx, &immediate, &screen, &mut View::new(), 600.0); assert_eq!(plain, framed); } #[test] fn a_frames_verbs_are_drawn_under_the_screen() { // goingson's compose window. The verbs belong to the mount, so they are // drawn without the screen describing them, and after it because an // immediate-mode host lays out in the order it is told. let ctx = egui::Context::default(); let immediate = renderer(); let screen = screen_of([Node::text("body")]); let frame = Frame::new() .offering(Act::new("Send", Action::post("/compose/send"))) .offering(Act::new("Discard", Action::post("/compose/discard"))); let shapes = framed_shapes(&ctx, &immediate, &screen, &frame, &mut View::new()); let body = painted_at(&shapes, "body"); let send = painted_at(&shapes, "Send"); let discard = painted_at(&shapes, "Discard"); assert!(send > body, "the frame is under the screen"); assert!(discard > send, "verbs keep the order they were declared in"); } #[test] fn a_banner_rests_in_a_reporting_frame_and_a_toast_still_floats() { // The status line without a channel: `Screen::notices` already carries the // messages, and a reporting mount changes where one of the two kinds lands // rather than adding a second way to say it. let ctx = egui::Context::default(); let immediate = renderer(); let mut screen = screen_of([Node::text("body")]); screen .notices .push(Node::banner(layout::Tone::Danger, "Not sent")); screen.notices.push(Node::Notice { kind: layout::Notice::Toast, tone: layout::Tone::Info, text: "Saved".into(), act: None, }); let shapes = framed_shapes( &ctx, &immediate, &screen, &Frame::new().reporting(), &mut View::new(), ); let body = painted_at(&shapes, "body"); assert!( painted_at(&shapes, "Not sent") > body, "a banner rests under the screen" ); assert!( painted_at(&shapes, "Saved") < body, "a toast floats above it as it always did" ); } // ── The panel the app keeps on screen ── /// One frame's painted shapes, with the app's chrome drawn as well. fn chromed_shapes( ctx: &egui::Context, immediate: &Immediate, screen: &Screen, frame: &Frame, chrome: &Chrome, view: &mut View, ) -> String { let input = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size( egui::Pos2::ZERO, egui::vec2(600.0, 900.0), )), ..Default::default() }; let output = ctx.run_ui(input, |ctx| { egui::CentralPanel::default().show(ctx, |ui| { immediate.chromed(ui, screen, frame, chrome, view); }); }); format!("{:?}", output.shapes) } #[test] fn an_app_that_declares_no_panel_draws_what_it_always_drew() { // The default has to be the old picture, or every host paints something new // the moment this member arrives. let ctx = egui::Context::default(); let immediate = renderer(); let screen = screen_of([Node::text("body")]); let framed = framed_shapes(&ctx, &immediate, &screen, &Frame::new(), &mut View::new()); let chromed = chromed_shapes( &ctx, &immediate, &screen, &Frame::new(), &Chrome::new(), &mut View::new(), ); assert_eq!(framed, chromed); } #[test] fn the_panel_is_drawn_under_the_frame() { // goingson's running-timer widget. It belongs to the app, so it is painted // without any screen describing it, and after the frame because the panel // outlives the mount the frame came from. let ctx = egui::Context::default(); let immediate = renderer(); let screen = screen_of([Node::text("body")]); let frame = Frame::new().offering(Act::new("Send", Action::post("/compose/send"))); let chrome = Chrome::new().presenting( "timer", quasi_router::Role::Activity, Node::text("00:12:04"), ); let shapes = chromed_shapes(&ctx, &immediate, &screen, &frame, &chrome, &mut View::new()); let body = painted_at(&shapes, "body"); let send = painted_at(&shapes, "Send"); let panel = painted_at(&shapes, "00:12:04"); assert!(send > body, "the frame is under the screen"); assert!(panel > send, "the panel is under the frame"); } #[test] fn an_answer_aimed_at_the_panel_lands_in_it_rather_than_being_reported_missing() { // How a timer ever moves: the panel carries an address, so a route that has // changed what it says reaches it the way it reaches any other region. let mut runtime = Runtime::new(screen_of([Node::text("body")])).with_chrome(Chrome::new().presenting( "timer", quasi_router::Role::Activity, Node::text("00:12:04"), )); runtime.apply( &Request::post("/timer/tick"), Response::fragment("timer", Node::text("00:12:05")), ); assert_eq!( runtime.chrome().panel("timer").map(|panel| &panel.content), Some(&Node::text("00:12:05")) ); // A description bug is still reported: the panel is one address, not a // catch-all for everything the screen does not have. assert!(runtime.screen().notices.is_empty()); } /// A screen holding one bespoke region with a described heading in it. fn with_a_transport() -> Screen { Screen::sidebar_content("Library") .with(Slot::handover("player", "media-transport").with(Node::section("Episode 4"))) } #[test] fn a_bespoke_region_draws_the_host_fill_under_the_blocks_the_description_owns() { // Decision 4's egui half, and the counterpart to `Webview::with_fill` and // `Tui::with_fill`: the renderer hands the space over. The ordering is the // arrangement `Containment::Opaque` describes -- a heading the description // owns above a canvas it does not. use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; let ctx = egui::Context::default(); let painting = Arc::new(AtomicBool::new(false)); let seen = Arc::clone(&painting); let immediate = renderer().with_fill("player", move |immediate: &Immediate, ui: &mut egui::Ui| { // The fill paints in the palette the screen around it is painted in, // which is the whole reason it is handed the renderer. seen.store(immediate.palette().page == palette().page, Ordering::SeqCst); ui.label("PLAYING"); }); let shapes = painted( &ctx, &immediate, &with_a_transport(), &mut View::new(), 600.0, ); assert!( painting.load(Ordering::SeqCst), "the fill drew, in the palette" ); assert!( painted_at(&shapes, "Episode 4") < painted_at(&shapes, "PLAYING"), "the fill goes under the described blocks" ); } #[test] fn a_bespoke_region_with_no_fill_draws_what_the_description_says_and_stops() { // What every host that offers no fill gets, and what this renderer did for // every host before `with_fill` existed. let ctx = egui::Context::default(); let shapes = painted( &ctx, &renderer(), &with_a_transport(), &mut View::new(), 600.0, ); assert!(shapes.contains("Episode 4")); assert!(!shapes.contains("PLAYING")); } #[test] fn a_fill_named_against_a_pane_is_ignored() { // A host reaching into a region the description already owns. All three // renderers keep the rule, and it is why a fill is not simply "a drawing // for this id". let ctx = egui::Context::default(); let screen = Screen::sidebar_content("Library") .with(Slot::new("player", RegionKind::Pane).with(Node::text("described"))); let immediate = renderer().with_fill("player", |_: &Immediate, ui: &mut egui::Ui| { ui.label("PLAYING"); }); let shapes = painted(&ctx, &immediate, &screen, &mut View::new(), 600.0); assert!(shapes.contains("described")); assert!(!shapes.contains("PLAYING")); } #[test] fn a_fill_naming_a_slot_the_screen_does_not_have_draws_nowhere() { let ctx = egui::Context::default(); let immediate = renderer().with_fill("elsewhere", |_: &Immediate, ui: &mut egui::Ui| { ui.label("PLAYING"); }); let shapes = painted( &ctx, &immediate, &with_a_transport(), &mut View::new(), 600.0, ); assert!(!shapes.contains("PLAYING")); } #[test] fn a_toast_goes_away_on_its_own_and_a_banner_stays() { // `4453bf82`. The description says which of the two a message is and never // says how long a toast keeps: the when is this renderer's, so this is the // host being the thing that takes it away. let mut runtime = Runtime::new(screen_of([Node::text("Tasks")])); runtime.apply( &Request::post("/tasks/1/done"), Response::from(Outcome::Screen(screen_of([Node::text("Done")]))) .toast(layout::Tone::Success, "Task completed"), ); runtime.apply( &Request::post("/sync"), Response::from(Outcome::Fragment { region: "main".to_owned(), node: Node::text("Done"), }) .banner(layout::Tone::Danger, "Sync is failing"), ); assert_eq!(runtime.screen().notices.len(), 2); let start = std::time::Instant::now(); assert!(!runtime.expires_at(start), "nothing goes early"); assert!( runtime .tick_in_at(start) .is_some_and(|wait| wait <= crate::LINGER), "and the host is told when to come back" ); assert!(runtime.expires_at(start + crate::LINGER + std::time::Duration::from_secs(1))); let left = &runtime.screen().notices; assert_eq!(left.len(), 1, "{left:?}"); assert!( matches!(&left[0], Node::Notice { text, .. } if text == "Sync is failing"), "the banner is the one that stays: {left:?}" ); // Nothing left on a clock, so the host may sleep as it did before. assert!(!runtime.expires_at(start + crate::LINGER * 4)); assert_eq!(runtime.tick_in_at(start), None); } #[test] fn a_toast_arriving_on_a_screen_lingers_from_when_the_screen_did() { // The other way a toast joins a screen: described onto one rather than said // by a response. let mut runtime = Runtime::new(screen_of([Node::text("Tasks")])); let arriving = screen_of([Node::text("Today")]).saying(Node::Notice { kind: layout::Notice::Toast, tone: layout::Tone::Info, text: "Welcome back".to_owned(), act: None, }); runtime.apply(&Request::get("/tasks"), Response::screen(arriving)); let landed = std::time::Instant::now(); assert!(!runtime.expires_at(landed)); assert_eq!(runtime.screen().notices.len(), 1); assert!(runtime.expires_at(landed + crate::LINGER + std::time::Duration::from_secs(1))); assert!(runtime.screen().notices.is_empty()); assert_eq!(runtime.tick_in_at(landed), None); } #[test] fn a_screen_asks_for_the_frame_its_readouts_need() { // egui sleeps between frames, so a stopwatch nobody asked to redraw is a // stopwatch that stops. This is the number `show` hands the context. let at = std::time::SystemTime::UNIX_EPOCH; let still = Runtime::new(screen_of([Node::text("Write the brief")])); assert_eq!(still.tick_in(), None); let stamped = Runtime::new(screen_of([Node::age(at)])); assert_eq!(stamped.tick_in(), Some(crate::COARSE)); // The finest of the kinds on the screen, so one wake serves both. let both = Runtime::new(screen_of([Node::age(at), Node::since(at)])); assert_eq!(both.tick_in(), Some(crate::TICK)); } #[test] fn a_readout_in_a_row_is_drawn_where_the_row_puts_it() { // The measured shape, and the one a cell walk could quietly drop: goingson // puts the elapsed time on a task row beside its title. let mut view = View::new(); let started = std::time::SystemTime::now() - std::time::Duration::from_secs(65); let row = quasi_router::Row::new("Write the brief").part(layout::RowPart::Meta, Node::since(started)); draw(&screen_of([Node::list([row])]), &mut view); // And in a table cell, which is the other run this renderer walks by hand. let cell = quasi_router::Cell::new("Write the brief"); draw( &screen_of([Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: vec![quasi_router::Column::new("Task")], rows: vec![quasi_router::Row::cells([ cell, quasi_router::Cell { content: vec![Node::since(started)], ..quasi_router::Cell::default() }, ])], more: None, }]), &mut view, ); } /// A file leaves by `handed` rather than by the return value, and the screen /// the control was pressed on is still the screen showing. #[test] fn a_file_answer_is_handed_over_and_changes_nothing_on_screen() { let mut runtime = Runtime::new(screen_of([Node::text("settings")])); let follow_up = runtime.apply( &Request::get("/data/export/json"), Response::file( "goingson-export.json", quasi_router::Accepted::media_type("application/json"), br#"{"tasks":[]}"#.to_vec(), ), ); assert_eq!(follow_up, None); let handed = runtime.handed().expect("the answer handed a file over"); assert_eq!(handed.name, "goingson-export.json"); assert_eq!(handed.bytes, br#"{"tasks":[]}"#); assert_eq!( handed.kind, quasi_router::Accepted::Type("application/json".into()) ); // Nothing on the screen moved: a file is not a region and not a place. assert_eq!(runtime.handed(), None); } /// An ask for a place leaves by `locating`, drains once, and leaves the screen /// the control was pressed on showing. #[test] fn an_ask_for_a_place_leaves_by_locating_and_changes_nothing_on_screen() { let mut runtime = Runtime::new(screen_of([Node::text("import")])); let follow_up = runtime.apply( &Request::post("/import/open"), Response::locate(quasi_router::Locating::folder( "Import folder", Action::post("/import/from"), "folder", )), ); assert_eq!(follow_up, None); let asking = runtime.locating().expect("the answer asked for a place"); assert_eq!(asking.sought, quasi_router::Sought::Folder); assert_eq!(asking.prompt, "Import folder"); // The picker is the host's furniture: nothing was drawn, nothing navigated, // and a second drain gets the ask no second time. assert_eq!(runtime.locating(), None); // What the reader picked comes back as an ordinary request, built by the // crate that stated the parameter name rather than by the host. let answered = asking .answered([quasi_router::Picked::new("/home/max/samples", "samples")]) .expect("a route to answer to"); assert_eq!( answered, Request::post("/import/from") .sending(quasi_router::Params::new().with("folder", "/home/max/samples")) ); } /// The save shape reaches the host whole, suggested name included, because the /// name is the reason the dialog is opened rather than a folder picker. #[test] fn a_save_ask_carries_its_suggested_name_out_to_the_host() { let mut runtime = Runtime::new(screen_of([Node::text("classifier")])); let follow_up = runtime.apply( &Request::post("/classifier/open"), Response::locate(quasi_router::Locating::new( quasi_router::Sought::Save { name: "drums-2026-08-25.afcl".into(), accept: vec![quasi_router::Accepted::suffix(".afcl")], }, "Export classifier", Action::post("/classifier/export"), "path", )), ); assert_eq!(follow_up, None); let asking = runtime.locating().expect("the answer asked for a place"); let quasi_router::Sought::Save { name, accept } = &asking.sought else { panic!("a save ask"); }; assert_eq!(name, "drums-2026-08-25.afcl"); assert_eq!(accept, &[quasi_router::Accepted::Suffix(".afcl".into())]); assert_eq!(asking.prompt, "Export classifier"); let answered = asking .answered([quasi_router::Picked::new( "/home/max/exports/drums.afcl", "drums.afcl", )]) .expect("a route to answer to"); assert_eq!( answered, Request::post("/classifier/export") .sending(quasi_router::Params::new().with("path", "/home/max/exports/drums.afcl")) ); } /// Several files picked at once are one call, which is what keeps a batched /// import a batch. #[test] fn several_picked_files_answer_the_ask_once() { let mut runtime = Runtime::new(screen_of([Node::text("import")])); let follow_up = runtime.apply( &Request::post("/import/open"), Response::locate(quasi_router::Locating::new( quasi_router::Sought::Files { accept: vec![quasi_router::Accepted::suffix(".wav")], }, "Import files", Action::post("/import/files"), "path", )), ); assert_eq!(follow_up, None); let asking = runtime.locating().expect("the answer asked for a place"); let answered = asking .answered([ quasi_router::Picked::new("/tmp/a.wav", "a.wav"), quasi_router::Picked::new("/tmp/b.wav", "b.wav"), ]) .expect("a route to answer to"); assert_eq!( answered, Request::post("/import/files").sending( quasi_router::Params::new() .with("path", "/tmp/a.wav") .with("path", "/tmp/b.wav") ) ); } /// A reader who backs out of the picker has answered nothing, so the host makes /// no call and the runtime is not told. Nothing to assert but the absence, and /// the absence is the design: a cancelled picker costs the screen nothing. #[test] fn a_screen_with_no_ask_hands_out_no_place() { let mut runtime = Runtime::new(screen_of([Node::text("import")])); assert_eq!(runtime.locating(), None); } /// The name is sanitised where the host cannot forget to do it. This runtime /// writes nothing itself, so a host taking the name at face value is exactly /// the failure -- `../../.ssh/authorized_keys` beside the process. #[test] fn a_handed_file_carries_a_name_no_host_can_traverse_with() { let mut runtime = Runtime::new(screen_of([Node::text("settings")])); runtime.apply( &Request::get("/data/export"), Response::file( "../../.ssh/authorized_keys", quasi_router::Accepted::suffix(".txt"), b"ssh-rsa".to_vec(), ), ); let handed = runtime.handed().expect("the answer handed a file over"); assert!(!handed.name.contains('/')); assert!(!handed.name.contains("..")); } /// A host that never drains it drops the download, which is the cost of keeping /// the writing out of this crate. A second file replaces the first rather than /// queueing: two files from one answer is not something the vocabulary says. #[test] fn a_second_file_replaces_the_one_the_host_never_took() { let mut runtime = Runtime::new(screen_of([Node::text("settings")])); for name in ["first.json", "second.json"] { runtime.apply( &Request::get("/data/export"), Response::file( name, quasi_router::Accepted::media_type("application/json"), b"{}".to_vec(), ), ); } assert_eq!( runtime.handed().map(|handed| handed.name), Some("second.json".to_owned()) ); } /// One frame of a runtime, with these key events delivered to it. /// /// The same context across frames, because focus is memory and the guard being /// tested reads the focus a previous frame left behind. fn press( ctx: &egui::Context, runtime: &mut Runtime, immediate: &Immediate, events: Vec, ) -> Step { let modifiers = events .iter() .find_map(|event| match event { egui::Event::Key { modifiers, .. } => Some(*modifiers), _ => None, }) .unwrap_or(egui::Modifiers::NONE); let input = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size( egui::Pos2::ZERO, egui::vec2(900.0, 700.0), )), events, modifiers, ..Default::default() }; let mut step = Step::Idle; let _ = ctx.clone().run_ui(input, |ui| { step = runtime.show(ui, immediate); }); step } fn key(key: egui::Key, modifiers: egui::Modifiers) -> Vec { vec![egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers, }] } #[test] fn a_box_with_the_focus_keeps_the_letters_a_binding_would_have_taken() { // `bc5528e2`. audiofiles has 26 shortcuts and 16 of them are bare letters, // and until this guard existed declaring one meant the tag field and the // search box stopped accepting that letter: the binding fired first and the // character never arrived. let chrome = Chrome::new().bind("s", "Sync", Action::get("/sync")).bind( "ctrl+t", "Theme", Action::get("/theme"), ); let mut runtime = Runtime::new(screen_of([Node::Field(Box::new(Field::new( layout::FieldKind::Text, "search", "Search", )))])) .with_chrome(chrome); let immediate = renderer(); let ctx = egui::Context::default(); // Nothing is focused yet, so the bare key is the app's. This is also the // control: without it a guard that suppressed everything would pass. assert_eq!( press( &ctx, &mut runtime, &immediate, key(egui::Key::S, egui::Modifiers::NONE) ), Step::Call(Request::get("/sync")), "a bare binding must still fire when no box is answering the keyboard" ); // Tab reaches the one focusable widget on the screen, which is the field. let _ = press( &ctx, &mut runtime, &immediate, key(egui::Key::Tab, egui::Modifiers::NONE), ); assert!( ctx.text_edit_focused(), "the harness never focused the field, so the assertions below prove nothing" ); // The letter belongs to the box now. assert_eq!( press( &ctx, &mut runtime, &immediate, key(egui::Key::S, egui::Modifiers::NONE) ), Step::Idle, "the binding took a letter the box was going to receive" ); // And the half the shipped app's broader rule would have lost: a held key // produces no character, so it was never the box's to begin with. assert_eq!( press( &ctx, &mut runtime, &immediate, key(egui::Key::T, egui::Modifiers::CTRL) ), Step::Call(Request::get("/theme")), "a modified binding stopped working while a box had the focus" ); } #[test] fn shift_is_typing_and_command_is_not() { // The rule the guard turns on, asserted where it is readable. Shift is how // a keyboard makes a capital letter, so shift+key is a box's; the other // three produce no character on any layout. use egui::Modifiers; assert!(types(Modifiers::NONE)); assert!(types(Modifiers::SHIFT)); assert!(!types(Modifiers::CTRL)); assert!(!types(Modifiers::ALT)); assert!(!types(Modifiers::COMMAND)); assert!(!types(Modifiers::CTRL | Modifiers::SHIFT)); } #[test] fn a_navigating_call_swaps_the_view_and_takes_the_overlay_with_it() { // `00ee7af5`. The same sentence the webview says with an anchor and the // terminal by pushing a screen: the whole view is being replaced, so the // call is made and whatever is floating over the place being left is put // away on the way out rather than left hanging over wherever the answer // lands. let chrome = Chrome::new().bind( "g", "Slow Reader", Action::get("/p/slow-reader").navigating(), ); let mut runtime = Runtime::new(screen_of([Node::text("discover")])).with_chrome(chrome); runtime.apply( &Request::get("/discover/suggest"), Response { outcome: Outcome::Over(screen_of([Node::text("suggestions")])), notice: None, address: None, invalidates: Vec::new(), }, ); assert!(runtime.overlaid()); let immediate = renderer(); let ctx = egui::Context::default(); let step = press( &ctx, &mut runtime, &immediate, key(egui::Key::G, egui::Modifiers::NONE), ); let Step::Call(request) = step else { panic!("a navigating call was not made: {step:?}"); }; assert_eq!(request.path, "/p/slow-reader"); // Before the answer, which is the point: nothing has come back yet and the // overlay is already gone. assert!(!runtime.overlaid()); } #[test] fn an_ordinary_call_leaves_the_overlay_where_it_is() { // The mark is what puts the overlay away. A call without one that answers a // fragment still answers it into what is showing. let chrome = Chrome::new().bind("g", "Refine", Action::get("/discover/refine")); let mut runtime = Runtime::new(screen_of([Node::text("discover")])).with_chrome(chrome); runtime.apply( &Request::get("/discover/suggest"), Response { outcome: Outcome::Over(screen_of([Node::text("suggestions")])), notice: None, address: None, invalidates: Vec::new(), }, ); let immediate = renderer(); let ctx = egui::Context::default(); let step = press( &ctx, &mut runtime, &immediate, key(egui::Key::G, egui::Modifiers::NONE), ); assert!(matches!(step, Step::Call(_)), "{step:?}"); assert!(runtime.overlaid()); } #[test] fn a_call_that_goes_elsewhere_is_handed_over_rather_than_made() { // goingson `3fb2526a`. A mount here is a viewport, and putting one up is // the host's the same way a file dialog is. So the runtime builds the // request and hands it over instead of making it: what comes back must be // `Mount` and not `Call`, or the screen replaces the one that asked for a // second surface rather than appearing beside it. let chrome = Chrome::new().bind( "n", "New message", Action::get("/compose/7") .elsewhere() .carrying("folder", "drafts"), ); let mut runtime = Runtime::new(screen_of([Node::text("x")])).with_chrome(chrome); let immediate = renderer(); let ctx = egui::Context::default(); let step = press( &ctx, &mut runtime, &immediate, key(egui::Key::N, egui::Modifiers::NONE), ); let Step::Mount(request) = step else { panic!("a call marked elsewhere was made here: {step:?}"); }; assert_eq!(request.path, "/compose/7"); // The view rides along, so the mount comes up on the place the control was // offered under rather than on a default. assert_eq!(request.carried.get("folder"), Some("drafts")); } #[test] fn a_bare_binding_does_not_answer_for_its_shifted_twin() { // audiofiles binds `f` to the forge and `shift+f` to Find similar, which is // two entries a table is entitled to hold and `matches_logically` cannot // tell apart: it ignores a shift the pattern never asked for, so the bare // one matched both and the shifted one was unreachable. Only visible once // bare letters could be declared at all. let chrome = Chrome::new() .bind("f", "Forge", Action::get("/forge")) .bind("shift+f", "Find similar", Action::post("/detail/similar")); let mut runtime = Runtime::new(screen_of([Node::text("x")])).with_chrome(chrome); let immediate = renderer(); let ctx = egui::Context::default(); assert_eq!( press( &ctx, &mut runtime, &immediate, key(egui::Key::F, egui::Modifiers::NONE) ), Step::Call(Request::get("/forge")) ); assert_eq!( press( &ctx, &mut runtime, &immediate, key(egui::Key::F, egui::Modifiers::SHIFT) ), Step::Call(Request::post("/detail/similar")), "the shifted binding is unreachable behind its bare twin" ); } /// The act names a box on the screen and the press puts its value in, and this /// renderer's answer to "where in it" is the end. #[test] fn a_control_that_deposits_a_value_puts_it_in_the_box_it_named() { let screen = screen_of([ Node::Field(Box::new( Field::new(layout::FieldKind::Text, "body", "Body").value("Intro. "), )), Node::Act(Act::new("kick.png", Action::local()).filling("body", "![](media/kick.png)")), ]); let mut host = Host::new(); host.settle(&screen); // Local, so what comes back names no route and the runtime's own guard is // what stops it becoming a call. See // `a_local_action_is_not_an_address_handed_to_the_host`. let fired = host .click(&screen, "kick.png") .expect("the press is noticed"); assert_eq!(fired.action, Action::local()); // After what the box was already showing, not over it. That is the whole // reason the member exists: a server-side append loses the draft. assert_eq!(host.view.edit("body"), Some("Intro. ![](media/kick.png)")); } /// Twice is twice. Nothing here is idempotent and nothing should be: a reader /// inserting two images pressed two cards. #[test] fn two_deposits_land_one_after_the_other() { let screen = screen_of([ Node::Field(Box::new(Field::new( layout::FieldKind::Text, "body", "Body", ))), Node::Act(Act::new("one", Action::local()).filling("body", "a")), Node::Act(Act::new("two", Action::local()).filling("body", "b")), ]); let mut host = Host::new(); host.settle(&screen); host.click(&screen, "one"); host.click(&screen, "two"); assert_eq!(host.view.edit("body"), Some("ab")); } /// An ordinary control writes into nothing, or every press on every screen /// starts touching a box. #[test] fn a_control_that_names_no_box_writes_into_none() { let screen = screen_of([ Node::Field(Box::new( Field::new(layout::FieldKind::Text, "body", "Body").value("Intro."), )), Node::Act(Act::new("Save", Action::post("/save"))), ]); let mut host = Host::new(); host.settle(&screen); let fired = host.click(&screen, "Save").expect("the route is called"); assert_eq!(fired.action, Action::post("/save")); assert_eq!(host.view.edit("body"), Some("Intro.")); } /// What the accessibility tree says a screen drew, as `(role, name)` pairs. /// /// egui builds it from the `WidgetInfo` each widget reports, so this is what a /// screen reader would be handed rather than a second opinion about it. fn announced(screen: &Screen, view: &mut View) -> Vec<(egui::accesskit::Role, String)> { let immediate = renderer(); let ctx = egui::Context::default(); ctx.enable_accesskit(); let input = || egui::RawInput { screen_rect: Some(egui::Rect::from_min_size( egui::Pos2::ZERO, egui::vec2(900.0, 600.0), )), ..Default::default() }; // Two passes: egui lays out against the previous frame, so the first sees // widgets at the wrong rect and a row's strip has no rect to claim yet. let _ = ctx.run_ui(input(), |ui| { immediate.screen(ui, screen, view); }); let out = ctx.run_ui(input(), |ui| { immediate.screen(ui, screen, view); }); out.platform_output .accesskit_update .expect("accesskit is on, so a tree was built") .nodes .iter() .map(|(_, node)| { ( node.role(), node.label() .or_else(|| node.value()) .unwrap_or_default() .to_owned(), ) }) .collect() } #[test] fn a_row_that_opens_is_announced_as_something_you_press() { // The row's press is a bare `ui.interact`, which registers no `WidgetInfo`, // so before 2026-08-22 this row reached the tree as nothing at all: it // worked under a mouse and did not exist for a keyboard or a screen reader. let screen = screen_of([Node::list([ quasi_router::Row::new("Standup").activate(Action::post("/notes/1")), quasi_router::Row::new("Review").activate(Action::post("/notes/2")), ])]); let mut view = View::new(); let drawn = announced(&screen, &mut view); for named in ["Standup", "Review"] { assert!( drawn .iter() .any(|(role, name)| *role == egui::accesskit::Role::Button && name == named), "the row that opens {named} is not in the tree: {drawn:?}" ); } } #[test] fn a_row_that_only_lists_claims_nothing() { // Nothing to press, so nothing to announce. The counterpart of the test // above, and what stops the fix from calling every row a button. let screen = screen_of([Node::list([quasi_router::Row::new("Standup")])]); let mut view = View::new(); let drawn = announced(&screen, &mut view); assert!( !drawn .iter() .any(|(role, _)| *role == egui::accesskit::Role::Button), "{drawn:?}" ); } #[test] fn a_table_row_that_opens_is_announced_once() { // The table half of the same defect the list half fixed. Claimed per cell, // so the announcement goes on the first column only: saying it per cell // would put one button per column in the tree, all called the same thing. let screen = screen_of([Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: vec![ quasi_router::Column::new("Name"), quasi_router::Column::new("Tempo"), ], rows: vec![ quasi_router::Row::cells(vec![ quasi_router::Cell::new("kick.wav"), quasi_router::Cell::new("90"), ]) .activate(Action::post("/files/1/open")), ], more: None, }]); let mut view = View::new(); let drawn = announced(&screen, &mut view); let named: Vec<_> = drawn .iter() .filter(|(role, name)| *role == egui::accesskit::Role::Button && name == "kick.wav") .collect(); assert_eq!(named.len(), 1, "once, not once per column: {drawn:?}"); } #[test] fn a_table_row_that_only_lists_claims_nothing() { let screen = screen_of([Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: vec![quasi_router::Column::new("Name")], rows: vec![quasi_router::Row::cells(vec![quasi_router::Cell::new( "kick.wav", )])], more: None, }]); let mut view = View::new(); let drawn = announced(&screen, &mut view); assert!( !drawn .iter() .any(|(role, _)| *role == egui::accesskit::Role::Button), "{drawn:?}" ); } /// A region says which control and which value bring it out, and this renderer /// answers it from the view it is already holding. What proves the region was /// not drawn is the buffer: drawing a field seeds one from the description, so /// a field with no buffer after a frame is a field that was never on the /// screen. #[test] fn a_region_whose_control_holds_nothing_is_not_drawn() { let screen = Screen::sidebar_content("Pricing").with( Slot::new("main", RegionKind::Pane) .with(Node::Field(Box::new(Field::new( layout::FieldKind::Checkbox, "pwyw", "Pay what you want", )))) .with(Node::Region( Slot::group("pwyw-settings") .revealed_by(quasi_router::Reveal::ticked("pwyw")) .with(Node::Field(Box::new( Field::new(layout::FieldKind::Number, "suggested", "Suggested price") .value("12"), ))), )), ); let mut view = View::new(); draw(&screen, &mut view); assert_eq!( view.edit("suggested"), None, "the section was drawn on a control holding nothing" ); // Ticked, and the same description draws it, with nothing asked for: the // region carries no call and the reveal is local. let mut view = View::new(); view.set("pwyw", quasi_router::Node::SELECTED); draw(&screen, &mut view); assert_eq!(view.edit("suggested"), Some("12")); } /// An untouched control holds what the description offered, so a section /// arrives already out on a form the server refilled. #[test] fn a_region_reads_the_value_the_description_offered() { let screen = Screen::sidebar_content("Licensing").with( Slot::new("main", RegionKind::Pane) .with(Node::Field(Box::new( Field::new(layout::FieldKind::Text, "license_kind", "Licence").value("custom"), ))) .with(Node::Region( Slot::group("dash-custom-license") .revealed_by(quasi_router::Reveal::holding("license_kind", "custom")) .with(Node::Field(Box::new( Field::new(layout::FieldKind::Text, "licence_text", "Terms") .value("All rights reserved"), ))), )), ); let mut view = View::new(); draw(&screen, &mut view); assert_eq!(view.edit("licence_text"), Some("All rights reserved")); } /// The two client renderers answer one condition the same way, which is what /// keeps a screen described once from being two screens. #[test] fn the_set_of_regions_that_do_not_apply_is_computed_from_the_view() { let screen = Screen::sidebar_content("Placement").with( Slot::new("main", RegionKind::Pane).with(Node::Region( Slot::group("offset-input") .revealed_by(quasi_router::Reveal::holding_one_of( "position", ["before", "after"], )) .with(Node::text("Offset")), )), ); let mut view = View::new(); let chrome = Chrome::new(); assert_eq!( crate::reveal::hidden(&screen, &chrome, &view).regions, vec!["offset-input"] ); view.set("position", "after"); assert!( crate::reveal::hidden(&screen, &chrome, &view) .regions .is_empty() ); view.set("position", "inline"); assert_eq!( crate::reveal::hidden(&screen, &chrome, &view).regions, vec!["offset-input"] ); } // One conditional question inside a form: `8fdb814c`, goingson's zone picker. /// goingson's event form. The condition is the question's own because a form's /// questions are a flat list, and this renderer answers it exactly as it /// answers a region's: the box is not drawn, and no route is asked. #[test] fn a_question_whose_control_holds_another_value_is_not_drawn() { let screen = Screen::sidebar_content("Event").with(Slot::new("main", RegionKind::Pane).with( Node::Form { marks: ::quasi_router::stage::Marks::none(), action: Action::post("/events"), submit: "Save".into(), fields: vec![ Field::new(layout::FieldKind::Text, "tz_kind", "Time zone").value("relative"), Field::new(layout::FieldKind::Text, "timezone", "Anchored to") .value("America/Denver") .revealed_by(quasi_router::Reveal::holding("tz_kind", "local")), ], }, )); // Drawing a field seeds a buffer from the description, so no buffer is a // question that was never on the screen. let mut view = View::new(); draw(&screen, &mut view); assert_eq!(view.edit("timezone"), None); let mut view = View::new(); view.set("tz_kind", "local"); draw(&screen, &mut view); assert_eq!(view.edit("timezone"), Some("America/Denver")); } // A question answered N times: `60d1753c`, ruled 2026-08-25. /// The reminders question goingson `8fdb814c` restores. fn reminders() -> Field { Field::new(layout::FieldKind::Number, "reminder", "Reminder").repeating( quasi_router::Repeat::answered(["300", "900"]) .most(8) .adding("Add reminder") .removing("Remove"), ) } fn reminders_form() -> Screen { screen_of([Node::Form { marks: ::quasi_router::stage::Marks::none(), action: Action::post("/events"), submit: "Save".into(), fields: vec![reminders()], }]) } /// Every slot is drawn, each under its own indexed name. What proves a slot was /// drawn is its buffer: drawing a field seeds one from the description. #[test] fn every_slot_of_a_repeating_question_is_drawn_under_its_own_name() { let mut view = View::new(); draw(&reminders_form(), &mut view); assert_eq!(view.edit("reminder[0]"), Some("300")); assert_eq!(view.edit("reminder[1]"), Some("900")); assert_eq!(view.edit("reminder[2]"), None); // And the bare name submits nothing: the question is the group, and the // answers are the slots. assert_eq!(view.edit("reminder"), None); } /// The third hard part, in the host with no document: adding a slot is a fact /// the view records, and nothing is asked of a route. #[test] fn adding_and_removing_a_slot_asks_no_route() { let screen = reminders_form(); let mut host = Host::new(); host.settle(&screen); assert_eq!(host.view.standing(&reminders()), 2); let fired = host.click(&screen, "Add reminder"); assert!(fired.is_none(), "adding a slot called a route"); assert_eq!(host.view.standing(&reminders()), 3); let fired = host.click(&screen, "Remove"); assert!(fired.is_none(), "removing a slot called a route"); assert_eq!(host.view.standing(&reminders()), 2); } /// One submit carrying every instance, which is the whole of what separates /// this from a list of forms. #[test] fn one_submit_carries_every_slot() { let screen = reminders_form(); let mut host = Host::new(); host.settle(&screen); host.click(&screen, "Add reminder"); host.view.set("reminder[2]", "7200"); host.settle(&screen); let fired = host.click(&screen, "Save").expect("the form submits"); let sent: Vec<(&str, &str)> = fired.payload.iter().collect(); assert_eq!( sent, [ ("reminder[0]", "300"), ("reminder[1]", "900"), ("reminder[2]", "7200"), ], "one submit, three answers" ); assert_eq!(fired.payload.repeated("reminder"), ["300", "900", "7200"]); } /// Removing a slot moves the answers after it up, buffers and all. Leaving them /// where they were would submit a hole under the name the reader emptied. #[test] fn removing_a_slot_moves_the_answers_after_it_up() { let field = Field::new(layout::FieldKind::Number, "reminder", "Reminder") .repeating(quasi_router::Repeat::answered(["300", "900", "3600"])); let mut view = View::new(); view.set("reminder[0]", "300"); view.set("reminder[1]", "900"); view.set("reminder[2]", "3600"); view.remove_slot(&field, 0); assert_eq!(view.standing(&field), 2); assert_eq!(view.edit("reminder[0]"), Some("900")); assert_eq!(view.edit("reminder[1]"), Some("3600")); assert_eq!(view.edit("reminder[2]"), None); } /// The floor and the ceiling are the description's, and no press gets past /// them. #[test] fn the_floor_and_the_ceiling_hold() { let capped = Field::new(layout::FieldKind::Number, "reminder", "Reminder") .repeating(quasi_router::Repeat::answered(["300", "900"]).most(2)); let mut view = View::new(); view.add_slot(&capped); assert_eq!(view.standing(&capped), 2, "the ceiling held"); let floored = Field::new(layout::FieldKind::Text, "guest", "Guest") .repeating(quasi_router::Repeat::answered(["ana"]).least(1)); let mut view = View::new(); view.remove_slot(&floored, 0); assert_eq!(view.standing(&floored), 1, "the floor held"); } /// A per-slot message belongs to its own box, and the question's own error is /// about the set. Both are drawn, and neither is the other. #[test] fn a_slot_carries_its_own_error() { let mut field = Field::new(layout::FieldKind::Number, "reminder", "Reminder") .repeating(quasi_router::Repeat::answered(["300", "-1"]).wrong(1, "Must be positive")); field.error = Some("At most eight reminders".into()); assert_eq!(field.instance(1).error.as_deref(), Some("Must be positive")); assert_eq!(field.instance(0).error, None); let mut host = Host::new(); let screen = screen_of([Node::Field(Box::new(field))]); host.settle(&screen); // Both messages are on the screen: the slot's under its box, the set's // under the question. host.find("Must be positive"); host.find("At most eight reminders"); } #[test] fn a_pending_region_draws_the_mark_and_not_a_spinner() { // `5eccb6aa`. `ui.spinner()` was the only true rotating animation in the // tree, and it was drawn for `Readiness::Pending` whatever the region was // waiting on. Asserted through accesskit rather than over pixels, which is // this module's rule: what matters is that the drawing happens and that it // is not a widget announcing itself as something else. let screen = Screen::sidebar_content("Test").with( Slot::new("payouts", RegionKind::Pane) .fed_by(Action::get("/dashboard/payouts").awaiting()) .pending(), ); let mut view = View::new(); draw(&screen, &mut view); } #[test] fn a_delivery_count_belongs_to_the_wait_that_was_running() { // Rule 1's other half. A count that survived into the next call would be // drawn against a payload it says nothing about, and a count arriving with // nothing outstanding has no wait to belong to at all. let mut view = View::new(); view.delivered(4_096); assert_eq!( view.progress_at(std::time::Instant::now()).delivered, None, "nothing is outstanding, so there is nothing for the count to describe" ); let action = Action::post("/media").awaiting_amount(41_943_040); view.await_on(Some(action)); view.delivered(10_485_760); let progress = view.progress_at(std::time::Instant::now()); assert_eq!(progress.delivered, Some(10_485_760)); assert!( progress.elapsed.is_some(), "the clock started with the call" ); // The next call starts clean rather than inheriting the last one's figure. view.await_on(Some(Action::post("/other").awaiting_amount(8))); assert_eq!(view.progress_at(std::time::Instant::now()).delivered, None); // And an answered call leaves nothing behind. view.await_on(None); assert_eq!(view.progress_at(std::time::Instant::now()).elapsed, None); } #[test] fn the_same_overlay_does_not_stack_on_itself() { let mut runtime = Runtime::new(screen_of([Node::text("under")])); let open = |runtime: &mut Runtime| { runtime.apply( &Request::get("/help"), Response { outcome: Outcome::Over(screen_of([Node::text("help")])), notice: None, address: None, invalidates: Vec::new(), }, ); }; open(&mut runtime); assert!(runtime.overlaid()); // Four more presses. Every one is a fresh route call answering an equal but // distinct screen value, which is why the guard is the request. for _ in 0..4 { open(&mut runtime); } // One layer, so one Escape rather than five. assert!(runtime.dismiss()); assert!(!runtime.overlaid()); assert!(!runtime.dismiss(), "and nothing left underneath it"); } /// The guard is the top layer only: a different overlay raised over one still /// stacks, which is what a confirm over a palette is. #[test] fn a_different_overlay_still_stacks_and_unwinds_in_order() { let mut runtime = Runtime::new(screen_of([Node::text("under")])); let raise = |runtime: &mut Runtime, path: &str, label: &str| { runtime.apply( &Request::get(path), Response { outcome: Outcome::Over(screen_of([Node::text(label)])), notice: None, address: None, invalidates: Vec::new(), }, ); }; raise(&mut runtime, "/palette", "palette"); raise(&mut runtime, "/confirm", "confirm"); // And the inner one refuses to stack on itself while it is on top. raise(&mut runtime, "/confirm", "confirm"); assert!(runtime.dismiss()); assert!(runtime.overlaid(), "the palette is still up"); // Back on the palette, its own identity is restored rather than lost, so it // still refuses to stack itself. raise(&mut runtime, "/palette", "palette"); assert!(runtime.dismiss(), "the palette refused to stack on itself"); assert!(!runtime.overlaid()); assert!(!runtime.dismiss()); } /// A host that has to read a gesture the description does not carry /// -- audiofiles' drag into a DAW -- could not tell a press on a row from a /// press on the toolbar, and could not say which row. The description hands back /// no geometry and should not; the renderer knows where it drew and now says so. #[test] fn a_host_can_ask_which_described_row_it_drew_under_a_point() { let immediate = renderer(); let screen = screen_of([Node::list([ quasi_router::Row::new("kick.wav").ticking("1", false), quasi_router::Row::new("snare.wav").ticking("2", false), ])]); let mut view = View::new(); let mut found = None; let mut off = None; let mut ctx = None; egui::__run_test_ui(|ui| { immediate.screen(ui, &screen, &mut view); // Inside the first row's strip. Taken off the `Ui` rather than guessed, // so this is not asserting a layout. let inside = ui.min_rect().left_top() + egui::vec2(4.0, 4.0); found = crate::row_at(ui.ctx(), inside); // Far outside anything drawn, which is the toolbar case. off = crate::row_at(ui.ctx(), egui::pos2(-500.0, -500.0)); ctx = Some(ui.ctx().clone()); }); let found = found.expect("a point inside the list is over a row"); assert_eq!(found.index, 0); assert_eq!(found.value.as_deref(), Some("1")); assert!(off.is_none(), "a point over nothing is over no row"); drop(ctx); } /// A table is the shape the one consumer actually uses, and a row there is the /// run of its cells rather than one widget. #[test] fn a_table_row_is_found_by_any_of_its_cells() { let immediate = renderer(); let screen = screen_of([Node::Table { marks: ::quasi_router::stage::Marks::none(), columns: vec![ quasi_router::Column::new("Name"), quasi_router::Column::new("Size"), ], rows: vec![ quasi_router::Row::cells([ quasi_router::Cell::new("kick.wav"), quasi_router::Cell::new("2.1 MB"), ]) .ticking("1", false), quasi_router::Row::cells([ quasi_router::Cell::new("snare.wav"), quasi_router::Cell::new("1.4 MB"), ]) .ticking("2", false), ], more: None, }]); let mut view = View::new(); let mut hits = Vec::new(); egui::__run_test_ui(|ui| { immediate.screen(ui, &screen, &mut view); let rect = ui.min_rect(); // Sweep the drawn area and collect which rows answered, rather than // asserting a coordinate this renderer never promised. let mut y = rect.top(); while y < rect.bottom() { if let Some(at) = crate::row_at(ui.ctx(), egui::pos2(rect.left() + 4.0, y)) { hits.push(at.value.clone()); } y += 2.0; } // Sweep the drawn area and collect which rows answered, rather than // asserting a coordinate this renderer never promised. let mut y = rect.top(); while y < rect.bottom() { if let Some(at) = crate::row_at(ui.ctx(), egui::pos2(rect.left() + 4.0, y)) { hits.push(at.value.clone()); } y += 2.0; } }); assert!( hits.iter().any(|value| value.as_deref() == Some("1")), "the first row answered somewhere: {hits:?}" ); } /// `Field::writes` fires when the value is complete rather than on the way to /// it, which is what `quasi-webview` has always meant by emitting it as `hx- /// trigger="change"`. This renderer fired on every keystroke, so typing 30 /// into a bounded box posted 3 on the way. #[test] fn a_change_fires_when_the_value_is_complete_and_not_on_the_way_to_it() { let screen = screen_of([ Node::Field(Box::new( Field::new(layout::FieldKind::Number, "row_height", "Row height") .value("20") .writes(Action::post("/settings/row-height")), )), Node::act("Elsewhere", Action::post("/elsewhere")), ]); let mut host = Host::new(); host.settle(&screen); // Into the box, which is what focuses it. host.click(&screen, "20"); // Typing. Every one of these was a write before, and 202 is exactly the // shape of the defect: outside the bounds the field's own hint states. for typed in ["2", "02"] { let fired = host.frame(&screen, vec![egui::Event::Text(typed.to_owned())]); assert!( fired.is_none(), "typing {typed} is on the way to a value, not a write" ); } // Leaving the box is what completes it. The press lands on another control, // so this is the ordinary way a reader finishes with a field. let fired = host.click(&screen, "Elsewhere"); let wrote = fired.is_some_and(|fired| fired.action.destination.route() == Some("/settings/row-height")); assert!(wrote, "leaving the box is the write"); } /// The other half: a value that never differs from what the description offered /// is not a write however the reader leaves the box, which is what a browser's /// `change` promises. #[test] fn leaving_a_box_untouched_writes_nothing() { let screen = screen_of([Node::Field(Box::new( Field::new(layout::FieldKind::Text, "title", "Title") .value("Kick") .writes(Action::post("/rename")), ))]); let mut host = Host::new(); host.settle(&screen); assert!(host.frame(&screen, Vec::new()).is_none()); } /// A press on a row of a live selection says what it meant, and the meaning is /// what this host reads off the keys rather than what the description carries. #[test] fn a_press_on_a_row_of_a_live_selection_says_how_it_was_meant() { let screen = screen_of([Node::list([ quasi_router::Row::new("kick.wav") .activate(Action::post("/files/1/open")) .choosing("1", false), quasi_router::Row::new("snare.wav") .activate(Action::post("/files/2/open")) .choosing("2", true), ])]); let mut host = Host::new(); host.settle(&screen); let meant = |fired: Option| { fired .expect("the row fired") .payload .get(quasi_router::Node::CHOOSING) .map(ToOwned::to_owned) }; // The plain press: this row and nothing else. assert_eq!( meant(host.click(&screen, "kick.wav")), Some("only".to_owned()) ); // Command, which is ctrl on everything but a Mac and which egui has already // resolved for us. assert_eq!( meant(host.click_holding(&screen, "kick.wav", egui::Modifiers::COMMAND)), Some("also".to_owned()) ); assert_eq!( meant(host.click_holding(&screen, "kick.wav", egui::Modifiers::SHIFT)), Some("through".to_owned()) ); // Both held is a range, matching every file manager: the one thing it // cannot mean is the plain press. assert_eq!( meant(host.click_holding( &screen, "kick.wav", egui::Modifiers::COMMAND | egui::Modifiers::SHIFT )), Some("through".to_owned()) ); } /// The other half, and it is what keeps this additive: a row that is not part of /// a live selection sends what it always sent. #[test] fn a_row_that_cannot_be_chosen_says_nothing_about_how_it_was_pressed() { let screen = screen_of([Node::list([ quasi_router::Row::new("kick.wav").activate(Action::get("/1")) ])]); let mut host = Host::new(); host.settle(&screen); let fired = host .click_holding(&screen, "kick.wav", egui::Modifiers::COMMAND) .expect("the row fired"); assert!( fired.payload.get(quasi_router::Node::CHOOSING).is_none(), "an ordinary row grew a parameter: {:?}", fired.payload ); } /// This host is the one that can anchor to a real rect, so it does: the /// renderer notes where it drew each region and each named control, and the /// runtime reads that back at draw time. #[test] fn the_renderer_says_where_it_drew_a_region_and_a_named_control() { let immediate = renderer(); let screen = Screen::sidebar_content("Files").with( Slot::new("browser", RegionKind::Pane) .with(Node::Act(Act::new("Sort", Action::get("/sort")).id("sort"))) .with(Node::text("rows")), ); let mut view = View::new(); let mut region = None; let mut control = None; let mut unnamed = None; let mut ctx = None; egui::__run_test_ui(|ui| { immediate.screen(ui, &screen, &mut view); region = crate::geometry::anchor_rect( ui.ctx(), &quasi_router::Anchor::Region("browser".into()), &[], ); control = crate::geometry::anchor_rect( ui.ctx(), &quasi_router::Anchor::Control("sort".into()), &[], ); // Nothing was named this, so there is nothing to point at. unnamed = crate::geometry::anchor_rect( ui.ctx(), &quasi_router::Anchor::Control("nothing".into()), &[], ); ctx = Some(ui.ctx().clone()); }); let region = region.expect("the region was drawn"); let control = control.expect("the named control was drawn"); // The control is inside the region that holds it, which is the check that // says these are the rects they claim to be rather than two defaults. assert!(region.contains_rect(control), "{control:?} in {region:?}"); assert!(unnamed.is_none(), "an unnamed control is not noted"); drop(ctx); } /// A menu over a selection opens against the set, which is the box around every /// ticked row that was actually drawn. #[test] fn a_selection_anchor_is_the_box_around_the_ticked_rows() { let immediate = renderer(); let screen = screen_of([Node::list([ quasi_router::Row::new("kick.wav").ticking("1", false), quasi_router::Row::new("snare.wav").ticking("2", false), ])]); let mut view = View::new(); let mut one = None; let mut both = None; let mut none = None; let mut ctx = None; egui::__run_test_ui(|ui| { immediate.screen(ui, &screen, &mut view); one = crate::geometry::anchor_rect( ui.ctx(), &quasi_router::Anchor::Selection, &["1".to_owned()], ); both = crate::geometry::anchor_rect( ui.ctx(), &quasi_router::Anchor::Selection, &["1".to_owned(), "2".to_owned()], ); // Nothing ticked is nothing to anchor to. none = crate::geometry::anchor_rect(ui.ctx(), &quasi_router::Anchor::Selection, &[]); ctx = Some(ui.ctx().clone()); }); let one = one.expect("the first row was drawn"); let both = both.expect("both rows were drawn"); assert!( both.contains_rect(one), "the set is the box around its members" ); assert!(none.is_none(), "an empty selection anchors nothing"); drop(ctx); } /// The runtime keeps an anchor it can resolve and drops one it cannot, so the /// draw path has one question to ask rather than two. #[test] fn an_anchor_that_names_nothing_is_dropped_and_the_menu_still_opens() { let mut runtime = Runtime::new(Screen::sidebar_content("Files").with(Slot::new("browser", RegionKind::Pane))); runtime.apply( &Request::get("/menu"), Response { outcome: Outcome::Anchored { screen: screen_of([Node::text("menu")]), anchor: quasi_router::Anchor::Region("browser".into()), }, notice: None, address: None, invalidates: Vec::new(), }, ); assert!(runtime.overlaid()); assert_eq!( runtime.anchor, Some(quasi_router::Anchor::Region("browser".into())) ); // Dismissing puts the screen back and takes the anchor with it. `dismiss` // rather than `back`: an anchored menu is not a place, so the layer pops // and history is never consulted. assert!(runtime.dismiss()); assert!(!runtime.overlaid()); assert_eq!(runtime.anchor, None); let mut runtime = Runtime::new(screen_of([Node::text("rows")])); runtime.apply( &Request::get("/menu"), Response { outcome: Outcome::Anchored { screen: screen_of([Node::text("menu")]), anchor: quasi_router::Anchor::Region("nowhere".into()), }, notice: None, address: None, invalidates: Vec::new(), }, ); // The menu opened all the same. What was lost is where it sits. assert!(runtime.overlaid()); assert_eq!(runtime.anchor, None); } /// The `900865dd` guard covers the anchored member too. #[test] fn an_anchored_menu_does_not_stack_on_the_same_request() { let mut runtime = Runtime::new(screen_of([Node::text("rows")])); let request = Request::get("/menu"); let answer = || Response { outcome: Outcome::Anchored { screen: screen_of([Node::text("menu")]), anchor: quasi_router::Anchor::Selection, }, notice: None, address: None, invalidates: Vec::new(), }; runtime.apply(&request, answer()); runtime.apply(&request, answer()); assert!(runtime.dismiss()); assert!(!runtime.overlaid()); } /// A tab group: three labelled panels, one of them showing. fn tabbed(shown: usize) -> Screen { Screen::sidebar_content("Help").with( Slot::new("body", RegionKind::Pane).with(Node::Region( Slot::new("strip", RegionKind::TabGroup) .showing_one(shown) .frame( "Shortcuts", Node::Region( Slot::new("first", RegionKind::Group) .with(Node::text("every key that works")), ), ) .frame( "Features", Node::Region( Slot::new("second", RegionKind::Group) .with(Node::text("what the app does")), ), ), )), ) } #[test] fn a_region_showing_one_child_draws_one_child_and_a_strip() { // This renderer drew the whole body whatever `Showing` said until // `showing_body` existed, so a described tab group came out as every panel // stacked with no strip. Both halves are asserted: the strip is there, and // the panel that is not up is not. let mut host = Host::new(); host.settle(&tabbed(0)); assert!(on_screen(&host, "Shortcuts"), "the strip is not drawn"); assert!(on_screen(&host, "Features"), "the strip is missing a tab"); assert!( on_screen(&host, "every key that works"), "the shown panel is not drawn" ); assert!( !on_screen(&host, "what the app does"), "a panel that is not up was drawn anyway" ); } #[test] fn the_description_says_which_tab_a_screen_arrives_on() { let mut host = Host::new(); host.settle(&tabbed(1)); assert!(on_screen(&host, "what the app does"), "the wrong tab is up"); assert!( !on_screen(&host, "every key that works"), "both tabs are up" ); } #[test] fn pressing_a_tab_moves_the_frame_and_asks_the_app_nothing() { // The carousel half of `showing_body`: the panels are here already, so // moving between them is local. A round trip to reveal bytes the reader has // already downloaded is what the webview's derivation refuses too. let mut host = Host::new(); let screen = tabbed(0); host.settle(&screen); let fired = host.click(&screen, "Features"); assert!(fired.is_none(), "moving to a local panel called a route"); host.settle(&screen); assert!( on_screen(&host, "what the app does"), "the tab did not move" ); assert!( !on_screen(&host, "every key that works"), "the old panel stayed up" ); } #[test] fn a_tab_over_a_routed_panel_calls_its_own_route() { // The other half, and MNW's shape: a panel behind `Slot::fed_by` is fetched // when its tab is pressed. The strip button carries that address and no // target -- the router answers with a fragment naming the slot it changed. let mut host = Host::new(); let screen = Screen::sidebar_content("Dashboard").with( Slot::new("body", RegionKind::Pane).with(Node::Region( Slot::new("strip", RegionKind::TabGroup) .showing_one(0) .frame( "Library", Node::Region( Slot::new("first", RegionKind::Group).with(Node::text("the library")), ), ) .frame( "Settings", Node::Region( Slot::new("second", RegionKind::Group) .fed_by(Action::get("/dashboard/tabs/settings")), ), ), )), ); host.settle(&screen); let fired = host.click(&screen, "Settings"); assert_eq!( fired.and_then(|fired| fired.action.route().map(str::to_owned)), Some("/dashboard/tabs/settings".to_owned()), "a routed panel's tab did not call it" ); } #[test] fn children_without_labels_get_previous_position_next() { // A carousel. Nothing here reads `RegionKind`'s name: what the children // carry is what picks the idiom, which is the rule all three renderers // derive from. let mut host = Host::new(); let screen = Screen::sidebar_content("Gallery").with( Slot::new("body", RegionKind::Pane).with(Node::Region( Slot::new("frames", RegionKind::Group) .showing_one(0) .with(Node::text("the first picture")) .with(Node::text("the second picture")), )), ); host.settle(&screen); assert!(on_screen(&host, "1 / 2"), "the position is not drawn"); assert!( !on_screen(&host, "the second picture"), "a carousel drew every frame" ); let fired = host.click(&screen, "Next"); assert!(fired.is_none(), "moving a carousel called a route"); host.settle(&screen); assert!( on_screen(&host, "the second picture"), "Next did not advance the frame" ); } #[test] fn a_lone_dismissible_child_is_a_summary_line_that_opens_and_shuts() { // `Showing::AtMostOne` is the one member where showing nothing is a resting // place, so the control has to shut as well as open. quasi-tui's `shown` is // a bare `usize` and cannot reach the closed state again; this one can. let mut host = Host::new(); let screen = Screen::sidebar_content("Settings").with( Slot::new("body", RegionKind::Pane).with(Node::Region( Slot::new("advanced", RegionKind::Group) .showing_at_most_one(None) .frame( "Advanced", Node::Region( Slot::new("inner", RegionKind::Group) .with(Node::text("the dangerous knobs")), ), ), )), ); host.settle(&screen); assert!( on_screen(&host, "Advanced"), "the summary line is not drawn" ); assert!( !on_screen(&host, "the dangerous knobs"), "a closed disclosure drew its contents" ); let fired = host.click(&screen, "Advanced"); assert!(fired.is_none(), "opening a disclosure called a route"); host.settle(&screen); assert!( on_screen(&host, "the dangerous knobs"), "the disclosure did not open" ); host.click(&screen, "Advanced"); host.settle(&screen); assert!( !on_screen(&host, "the dangerous knobs"), "the disclosure would not shut again" ); } #[test] fn a_region_showing_everything_draws_what_it_always_drew() { // The additive claim. Nothing written before `Showing` existed changes, and // no chrome is derived for a region that shows its whole body. let mut host = Host::new(); let screen = screen_of([Node::text("first"), Node::text("second")]); host.settle(&screen); assert!(on_screen(&host, "first") && on_screen(&host, "second")); assert!(!on_screen(&host, "1 / 2"), "a plain region grew a counter"); } #[test] fn the_caret_is_owed_to_the_question_the_screen_named_and_only_on_arrival() { // Focus is egui's here, so what this renderer owes is the one-frame request // rather than the caret. The claim is taken by the frame that spends it: a // flag left standing would ask again every frame and the reader could never // move off the box. let screen = screen_of([Node::Form { marks: ::quasi_router::stage::Marks::none(), action: Action::post("/login"), submit: "Log in".into(), fields: vec![ Field::new(layout::FieldKind::Text, "email", "Email"), Field::new(layout::FieldKind::Secret, "password", "Password"), ], }]) .opening_at("password"); let mut runtime = Runtime::new(screen); // The runtime read it on arrival, so the first frame spends it and the // second has nothing to spend. assert!(runtime.view_mut().claims_caret("password")); assert!(!runtime.view_mut().claims_caret("password")); } #[test] fn a_screen_that_names_no_question_owes_the_caret_to_nobody() { let mut runtime = Runtime::new(screen_of([Node::field(Field::new( layout::FieldKind::Text, "email", "Email", ))])); assert!(!runtime.view_mut().claims_caret("email")); } #[test] fn drawing_a_band_draws_it_and_fires_nothing_by_itself() { // egui's harness answers responses rather than pixels, so what can be // asserted here is that the band lays out and that nothing in it goes off // without a press. The placement claim -- above the screen, notices // included -- is the order of the calls in `chromed` and is asserted in // quasi-tui, where a buffer can be read. use quasi_router::{Band, Brand, Place}; let chrome = Chrome::new() .offering(Place::new("discover", "Discover", Action::get("/discover"))) .banded( Band::new() .branded(Brand::new("Makenot.work", Action::get("/")).marking(".")) .searching(Field::new(layout::FieldKind::Text, "q", "Search")), ); let screen = screen_of([Node::text("body")]).at_place("discover"); let immediate = renderer(); let mut view = View::new(); let mut fired = None; egui::__run_test_ui(|ui| { fired = immediate.chromed(ui, &screen, &Frame::new(), &chrome, &mut view); }); assert!(fired.is_none(), "the band called a route nobody pressed"); // The default has to be the old window, or every app grows a header the // moment this member arrives. let mut plain = View::new(); let mut nothing = None; egui::__run_test_ui(|ui| { nothing = immediate.chromed(ui, &screen, &Frame::new(), &Chrome::new(), &mut plain); }); assert!(nothing.is_none()); }