//! A select whose options mark themselves, derived and filled. //! //! The one shape in this crate written for a feature rather than ported from a //! screen, and it is here because the feature is what makes a select derivable //! at all. Before per-option `chosen` a picker said which option was marked //! once, at the field, as a value the renderer compared against each option -- //! and a residual holds ONE compiled body per loop, so "exactly one row //! differs" was not something the body could carry. The derivation rendered the //! value as a sentinel, it matched no option, and the residual baked a //! synthesised unmatched option with the real one unmarked. //! //! Said per option it is a branch inside the row body, which is a shape a //! residual has. That is the whole of the claim, and the two tests below are //! it: the derivation finds a branch inside the loop, and filling reproduces //! the renderer byte for byte with a theme marked and with none. //! //! MNW's `/dashboard/tabs/ssh-keys` is the screen that wanted it. // 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 quasi_declare::declare; use quasi_router::Choice; /// One theme the picker offers. /// /// Carries `selected` per row, which is the fact the old spelling collapsed to /// one string for the renderer to re-derive. pub(crate) struct Theme { pub id: String, pub name: String, pub selected: bool, } impl Theme { fn new(id: &str, name: &str, selected: bool) -> Self { Self { id: id.into(), name: name.into(), selected, } } } /// Three themes with the second one marked. pub(crate) fn themes(marked: Option) -> Vec { ["dark", "light", "paper"] .iter() .enumerate() .map(|(index, id)| Theme::new(id, id, marked == Some(index))) .collect() } declare! { /// A range selector: one chip per range, the current one held down. /// /// MNW's analytics screens are the site, twice. `latched` settles the tag /// its own statement produces, and it sits in a member's body rather than /// an argument's -- the other place a settling setting turns up, and the /// one that reaches through a loop. #[staged] pub(crate) shape ranges(chosen: &str) -> Vec; for range in RANGES { chip range.to_string() to get "/analytics?range={range}" navigating { latched when is_shown(range, chosen); } } } declare! { /// The region the chips are spliced into. /// /// A shape answering a run of members is derived through whatever holds /// them, never on its own: its twin answers `Staged>`, and the /// marks in that only mean anything once a container has said where the /// members landed. #[staged] pub(crate) shape range_bar(chosen: &str) -> Node; region "RANGES" as Pane { include each ranges(chosen); } } /// The ranges a selector offers. pub(crate) const RANGES: &[&str] = &["7d", "30d", "90d", "all"]; /// Whether this chip is the one held down. pub(crate) fn is_shown(range: &str, chosen: &str) -> bool { range == chosen } /// What the settings form was asked to draw. /// /// The disclosure is the fact a guarded field turns on: MNW's mail settings put /// six server questions on the form only while it is open. pub(crate) struct Prefs { pub themes: Vec, pub advanced: bool, } declare! { /// The theme picker with an advanced question behind a disclosure. /// /// A form's fields are a bare list, so a guard on one is a marked run over /// the wrapper they accrete into and the form absorbs it. The field either /// side of the guarded one is what catches a mark that failed to move. #[staged] pub(crate) shape settings(prefs: &Prefs) -> Node; form put "/api/users/me/console" awaiting { submit "Save"; field Select "theme_id" "Console theme" { for theme in prefs.themes.iter() { option Choice::new(theme.id.clone(), theme.name.clone()) { chosen when theme.selected; } } } field Text "endpoint" "Server" when prefs.advanced; field Text "note" "Note"; } } declare! { /// The theme picker. #[staged] pub(crate) shape picker(themes: &[Theme]) -> Node; form put "/api/users/me/console-theme" awaiting { submit "Save Theme"; field Select "theme_id" "Console theme" { for theme in themes.iter() { option Choice::new(theme.id.clone(), theme.name.clone()) { chosen when theme.selected; } } } } } #[cfg(test)] mod tests { use quasi_http::Serves as _; use quasi_router::stage::{Op, Plan, Residual}; use quasi_webview::Webview; use super::*; fn residual() -> Residual { quasi_webview::stage::derive(&Webview::new(), picker_staged) } /// The branch is inside the loop, which is the whole point: the rows are /// one compiled body and what differs between them is a branch in it. fn prefs(advanced: bool) -> Prefs { Prefs { themes: themes(Some(1)), advanced, } } /// A guarded field is a branch, and the fields either side stay outside it. /// A settling setting inside a loop, which is one arm per pass. /// /// The Select's problem stated on the other control: exactly one chip /// differs and a residual holds one compiled loop body, so the difference /// has to be an arm inside that body rather than a branch beside it. #[test] fn a_latched_chip_is_an_arm_inside_the_loop() { let webview = Webview::new(); let residual = quasi_webview::stage::derive(&Webview::new(), range_bar_staged); for chosen in ["7d", "all", "none of them"] { assert_eq!( webview.fragment(&range_bar(chosen)), range_bar_serve(&residual, chosen), "chosen {chosen}" ); } } #[test] fn a_guarded_field_is_a_branch_in_the_form() { let webview = Webview::new(); let residual = quasi_webview::stage::derive(&Webview::new(), settings_staged); for advanced in [false, true] { let prefs = prefs(advanced); assert_eq!( webview.fragment(&settings(&prefs)), settings_serve(&residual, &prefs), "advanced {advanced}" ); } } #[test] fn the_marked_option_is_a_branch_inside_the_row() { let residual = residual(); fn branch_under_loop(ops: &[Op], inside: bool) -> bool { ops.iter().any(|op| match op { Op::Lit(_) | Op::Hole { .. } => false, Op::Branch(body) => inside || branch_under_loop(body, inside), Op::Arms(arms) => inside || arms.iter().any(|arm| branch_under_loop(arm, inside)), Op::Loop(body) => branch_under_loop(body, true), }) } assert!( branch_under_loop(residual.ops(), false), "{:#?}", residual.ops() ); } /// The derivation reads a loop and a guard rather than baking the render it /// happened to see. #[test] fn replaying_the_residual_reproduces_the_staged_render() { let webview = Webview::new(); let residual = residual(); fn replay(ops: &[Op], rows: usize, out: &mut String) { for op in ops { match op { Op::Lit(text) => out.push_str(text), Op::Hole { scope, id } => { out.push_str(&quasi_router::stage::sentinel_at(*scope, *id)); } Op::Branch(body) => replay(body, rows, out), Op::Arms(arms) => replay(&arms[0], rows, out), Op::Loop(body) => { for _ in 0..rows { replay(body, rows, out); } } } } } for rows in [1, 2, 5] { let mut replayed = String::new(); replay(residual.ops(), rows, &mut replayed); assert_eq!( webview.fragment(&picker_staged(&Plan::full(rows))), replayed, "the residual and the renderer disagree at {rows} rows" ); } } /// Filling it is what the renderer would have produced, marked and unmarked. /// /// The second case is the one the old spelling could not serve at all: a /// picker with nothing chosen rendered the guard's absent value as a /// sentinel and baked an unmatched option around it. #[test] fn a_filled_picker_is_what_the_renderer_would_have_produced() { let webview = Webview::new(); let residual = residual(); for marked in [Some(0), Some(1), Some(2), None] { let themes = themes(marked); let filled = picker_serve(&residual, &themes); assert_eq!( webview.fragment(&picker(&themes)), filled, "marked {marked:?}" ); assert_eq!( filled.matches(" selected").count(), usize::from(marked.is_some()), "{filled}" ); } } /// An empty list still fills, and marks nothing. #[test] fn a_picker_with_no_themes_goes_through_the_same_residual() { let webview = Webview::new(); let residual = residual(); let themes = Vec::new(); assert_eq!( webview.fragment(&picker(&themes)), picker_serve(&residual, &themes) ); } }