//! 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, Field, Message, Method, Node, Outcome, RegionKind, Request, Response, Screen, Slot, layout, }; use crate::{Immediate, Runtime, Step, View}; 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_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); }); } #[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. let mut view = View::new(); let screen = screen_of([ 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::Token(quasi_router::Tag::badge("beta")), Node::Meter(quasi_router::Meter::new(3, 6)), Node::Field(Box::new(Field::new( layout::FieldKind::Text, "title", "Title", ))), Node::list([quasi_router::Row::new("One")]), ]); draw(&screen, &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 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 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_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, Cells, Column}; let columns = vec![Column::new("name"), Column::new("bpm")]; let rows = vec![ Cells::new(["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. Cells::new([ Cell::new("snare.wav"), Cell::acts([Act::new("Play", Action::post("/play"))]), ]), ]; let mut view = View::new(); let screen = screen_of([Node::Table { columns, rows }]); 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 { columns: Vec::new(), rows: Vec::new(), }]); draw(&screen, &mut view); }