//! A dispatch, derived and filled. //! //! The other half of what `Op::Arms` is for, and the one the refusal was //! written about. A dispatch's cases replace each other at one position, so //! there is no insertion to measure and no deletion to put back: the staged //! twin read `plan.arm(id)`, which answered zero, and the residual held that //! one case as a literal with the others simply gone. MNW's forum settings pane //! compiled to "You haven't joined any forum communities yet." and would have //! served that to every member. //! //! Now the twin is told how many cases there are, the derivation renders once //! per case, and the arms are what they disagree over. //! //! MNW's feed is the screen that wanted it: a price cell is a badge when an //! item is free and text when it is not, which is one question with two shapes //! rather than two questions. // The fixture data below is built by this module's tests and by nothing else: // the bench itself measures the shapes, not the rows behind them. #![allow(dead_code)] use makeover_layout as layout; use quasi_declare::declare; use quasi_router::screen::Tag; /// One item, priced or free. pub(crate) struct Item { pub name: String, pub price: String, pub free: bool, } /// What the price cell reads, which is nothing when the item is free. fn price(item: &Item) -> String { if item.free { String::new() } else { item.price.clone() } } pub(crate) fn items(free: &[bool]) -> Vec { free.iter() .enumerate() .map(|(n, free)| Item { name: format!("item{n}"), price: format!("${n}.00"), free: *free, }) .collect() } declare! { /// The listing, whose price cell has two shapes. #[staged] pub(crate) shape listing(items: &[Item]) -> Node; table { column "Name" { width Fill; } column "Price" { width Content; } for item in items.iter() { include row(item); } } } declare! { /// The same listing with the dispatch written inline, and a cell after it. /// /// MNW's feed is this shape rather than the one below: the dispatch sits in /// the loop body of the table's own shape, with cells either side of it and /// an `activate` on the row. The version below puts the row in a shape of /// its own, which gives the dispatch a scope to itself and a position at /// the end -- so it never exercised what follows an arm. #[staged] pub(crate) shape inline_listing(items: &[Item]) -> Node; table { column "Name" { width Fill; } column "Price" { width Content; } column "Date" { width Content; } for item in items.iter() { cells { cell at "Name" item.name.clone(); given item.free { true -> cell at "Price" "" { token Tag::badge("Free").tone(layout::Tone::Success); } otherwise -> cell at "Price" price(item); } cell at "Date" item.name.clone(); activate to get "/i/{item.name}" navigating; } } } } declare! { /// One row. The price cell is a dispatch, not a pair of guards. /// /// Said as two guarded cells it would be one question written twice -- /// wiki `quasi-declare-form` section 24's first rule -- and the derivation /// varies guards one at a time, so it would render a Price column holding /// two cells, which the screen never produces. #[staged] pub(crate) shape row(item: &Item) -> Row; cells { cell at "Name" item.name.clone(); given item.free { true -> cell at "Price" "" { token Tag::badge("Free").tone(layout::Tone::Success); } otherwise -> cell at "Price" price(item); } } } #[cfg(test)] mod tests { use quasi_http::Serves as _; use quasi_router::stage::{Op, Residual}; use quasi_webview::Webview; use super::*; fn residual() -> Residual { quasi_webview::stage::derive(&Webview::new(), listing_staged) } /// The dispatch is arms, and both cases are in the residual. /// /// The old failure stated as a test: exactly one case reached the residual /// and the rest were gone, so a residual holding only one is the bug. /// A dispatch with a cell after it, which is the feed's shape. #[test] fn an_arm_is_followed_by_the_cells_written_after_it() { let webview = Webview::new(); let residual = quasi_webview::stage::derive(&Webview::new(), inline_listing_staged); let rows = items(&[true, false, true]); assert_eq!( webview.fragment(&inline_listing(&rows)), inline_listing_serve(&residual, &rows), ); } #[test] fn both_cases_reach_the_residual() { let residual = residual(); fn arms(ops: &[Op]) -> Option { ops.iter().find_map(|op| match op { Op::Arms(arms) => Some(arms.len()), Op::Branch(body) | Op::Loop(body) => arms(body), Op::Lit(_) | Op::Hole { .. } => None, }) } assert_eq!(arms(residual.ops()), Some(2), "{:#?}", residual.ops()); } /// Filled, it is what the renderer builds, for every mix of the two cases. #[test] fn a_filled_listing_is_what_the_renderer_would_have_produced() { let webview = Webview::new(); let residual = residual(); for mix in [ vec![], vec![false], vec![true], vec![false, true], vec![true, false], vec![true, true, false], ] { let items = items(&mix); assert_eq!( webview.fragment(&listing(&items)), listing_serve(&residual, &items), "{mix:?}" ); } } /// And the two cases really are different markup, so the test above is /// checking something. #[test] fn a_free_item_and_a_priced_one_draw_differently() { let residual = residual(); let free = listing_serve(&residual, &items(&[true])); let priced = listing_serve(&residual, &items(&[false])); // The price cell specifically: the name cell carries `cell-value` in // both, which is the point of checking the column rather than the row. fn price_cell(html: &str) -> &str { // `class="cell ` and not just the column name: the heading row // carries the same column token. let at = html.find("class=\"cell col-Price").expect("a price cell"); let rest = &html[at..]; &rest[..rest.find("").map_or(rest.len(), |to| to + 6)] } assert!(price_cell(&free).contains("badge"), "{free}"); assert!(!price_cell(&free).contains("cell-value"), "{free}"); assert!(price_cell(&priced).contains("cell-value"), "{priced}"); assert!(!price_cell(&priced).contains("badge"), "{priced}"); } }