//! The same screen, declared. //! //! [`crate::staging`] writes MNW's `library_contacts` pane by hand, against a //! `Data` seam invented for the spike. This is the screen as the server //! actually holds it: a verbatim port of the three `declare!` blocks in //! `MNW/server/src/quasi/library_contacts.rs`, against the bench's own rows. //! //! Two things it has that the hand-written copy does not, and both are the //! point. Every table is placed behind a guard, so the branch case the spike's //! module doc calls its real constraint is exercised here rather than assumed //! away. And a shape reaches another shape by `include`, so the residual has to //! compose rather than being one flat program. //! //! Kept verbatim rather than tidied. A bench measuring a shape written for the //! bench measures the bench. use quasi_declare::declare; use quasi_router::Node; use quasi_router::screen::Tag; use crate::fixture::{Buyer, Rows, Shared}; /// The region the tab nav targets, as the server names it. const REGION: &str = "tab-content"; /// This screen's own nest, which the revoke control addresses under. const PATH: &str = "/library/tabs/contacts"; declare! { /// Everything inside the tab pane. #[staged] pub(crate) shape pane(buyers: &[Buyer], shared: &[Shared]) -> Node; region REGION as Pane { empty "No contacts yet." when buyers.is_empty() and shared.is_empty(); section "Your Buyers ({buyers.len()})" unless buyers.is_empty(); text "Buyers who opted to share their email with you at purchase time." unless buyers.is_empty(); include buyers_table(buyers) unless buyers.is_empty(); section "Shared With" unless shared.is_empty(); text "You've shared your email with these creators. You can revoke sharing at any time." unless shared.is_empty(); include shared_table(shared) unless shared.is_empty(); } } declare! { /// The buyers who shared an email. #[staged] pub(crate) shape buyers_table(buyers: &[Buyer]) -> Node; table { column "Username" { width Content; priority Essential; } column "Email" { width Fill; priority Essential; } column "Purchases" { width Content; } column "Total Spent" { width Content; } column "Last Purchase" { width Content; priority Optional; } for buyer in buyers.iter() { cells { cell buyer.username.clone() { activate to get "/u/{buyer.username}" navigating; } cell buyer.email.clone() { activate to external "mailto:{buyer.email}"; } cell buyer.purchases.clone(); cell buyer.spent.clone(); cell buyer.last_purchase.clone(); } } } } declare! { /// The same table with the guarded cell holding text rather than a token. /// /// Refused rather than compiled, and the refusal is the point. A cell whose /// content is one piece of text makes its column write `cell-value`, so a /// guard on it varies the block's class AND the block's contents. Those are /// two places and a branch is one, so the derivation says so instead of /// compiling the half it can see and serving a row with a stray class. #[staged] pub(crate) shape steered_table(buyers: &[Buyer]) -> Node; table { column "Username" { width Content; } column "Note" { width Content; } for buyer in buyers.iter() { cells { cell at "Username" buyer.username.clone(); cell at "Note" buyer.spent.clone() unless buyer.spent.is_empty(); } } } } declare! { /// A table whose last column is answered by only some rows. /// /// The case MNW's repository listing is, and the one that says why a cell's /// placement has two halves. A column emits its `
` for every row /// whether or not a cell answers it, so a guarded cell's branch covers what /// the cell WROTE and not the block around it. Covering the block deletes a /// column the row still draws, and every row after it shifts one place /// left. #[staged] pub(crate) shape sparse_table(buyers: &[Buyer]) -> Node; table { column "Username" { width Content; } column "Note" { width Content; } for buyer in buyers.iter() { cells { cell at "Username" buyer.username.clone(); // A token rather than the text itself, and that is the whole of // why this one compiles. A cell holding one piece of text makes // its column write `cell-value`, so guarding it would vary the // block's class as well as its contents -- two places, and a // branch is one. `quasi-webview` refuses that case by name; see // the test below. cell at "Note" "" unless buyer.spent.is_empty() { token Tag::badge(buyer.spent.clone()); } } } } } declare! { /// The creators this reader has shared an email with. #[staged] pub(crate) shape shared_table(shared: &[Shared]) -> Node; table { column "Creator" { width Fill; priority Essential; } column "" { width Content; priority Essential; } for creator in shared.iter() { cells { cell creator.name.clone() { activate to get "/u/{creator.username}" navigating; } cell "" { act "Revoke" to delete "{PATH}/revoke/{creator.seller_id}" awaiting { confirm "Revoke contact sharing with {creator.username}?"; tone Danger; } } } } } } /// The pane, built from real rows the way a request builds it. #[must_use] pub fn describe(rows: &Rows) -> Node { pane(&rows.buyers, &rows.shared) } #[cfg(test)] mod tests { use quasi_http::Serves as _; use quasi_webview::Webview; use super::*; use crate::fixture::SIZES; /// The port is faithful to the server's screen, which is not the same /// thing as agreeing with [`crate::staging`]. /// /// The hand-written copy is the impoverished one. It was written for the /// spike against a `Data` seam and dropped four things the declaration /// carries, each asserted below so that the difference is a record rather /// than a surprise the next reader has to rediscover: /// /// - `navigating` on a profile link, which drops the htmx swap because a /// profile is a whole document rather than a region. /// - `confirm`, `tone Danger` and `awaiting` on the Revoke control. /// /// Everything the acceptance rule protects does agree: every address, the /// actions and their methods, and the heading structure. That is checked /// here, and the bytes deliberately are not. #[test] fn the_declared_pane_carries_what_the_hand_written_one_dropped() { let webview = Webview::new(); let rows = Rows::new(5); let hand = webview.fragment(&crate::fixture::pane(&rows)); let real = webview.fragment(&describe(&rows)); // What the declaration says and the copy could not. assert!(real.contains(r#"data-tone="danger""#), "{real}"); assert!(real.contains("hx-confirm="), "{real}"); assert!(real.contains(r#"data-awaiting="indeterminate""#), "{real}"); assert!(!hand.contains("hx-confirm="), "{hand}"); // `navigating`: the anchor is the whole of it, so no swap is asked for. assert!(!real.contains(r#"hx-get="/u/buyer0000""#), "{real}"); assert!(hand.contains(r#"hx-get="/u/buyer0000""#), "{hand}"); // What must survive regardless, which is the acceptance rule. for size in SIZES { let rows = Rows::new(size); let real = webview.fragment(&describe(&rows)); for i in 0..size { assert!(real.contains(&format!(r#"href="/u/buyer{i:04}""#)), "{i}"); assert!( real.contains(&format!(r#"href="mailto:buyer{i:04}@example.com""#)), "{i}" ); assert!( real.contains(&format!( r#"hx-delete="/library/tabs/contacts/revoke/{:08}""#, i * 31 + 7 )), "{i}" ); } assert_eq!( real.matches("table-row").count(), size * 2, "at {size} rows" ); assert!(real.contains(&format!("Your Buyers ({size})")), "at {size}"); } } /// The guards the hand-written copy has no way to carry. #[test] fn no_rows_places_the_empty_line_and_neither_table() { let html = Webview::new().fragment(&describe(&Rows::new(0))); assert!(html.contains("No contacts yet."), "{html}"); assert!(!html.contains("Your Buyers"), "{html}"); assert!(!html.contains("Shared With"), "{html}"); } } #[cfg(test)] mod staged_tests { use quasi_http::Serves as _; use quasi_router::stage::{Plan, read_sentinel}; use quasi_webview::Webview; use super::*; #[test] fn the_staged_twin_renders_the_screen_with_sentinels_where_values_go() { let html = Webview::new().fragment(&pane_staged(&Plan::full(1))); // The literals are the renderer's own, so the markup is the real // screen's and not something the macro decided. assert!(html.contains(r#"id="tab-content""#), "{html}"); assert!(html.contains("Your Buyers ("), "{html}"); assert!(html.contains(r#"class="table-head""#), "{html}"); assert!(html.contains("Last Purchase"), "{html}"); assert!(html.contains("Revoke"), "{html}"); // The consts stayed real: a value that is the same for every request // belongs in the literal rather than in a hole. assert!(html.contains("/library/tabs/contacts/revoke/"), "{html}"); // Every per-request value became a sentinel, addresses included. let mut holes = Vec::new(); let mut rest = html.as_str(); while let Some(at) = quasi_router::stage::find_sentinel(rest) { if let Some(hole) = read_sentinel(&rest[at..]) { holes.push(hole); rest = &rest[at + quasi_router::stage::SENTINEL_LEN..]; } else { rest = &rest[at + 3..]; } } assert!(!holes.is_empty(), "no sentinels at all: {html}"); // The two tables were reached by two includes, so their holes are in // different scopes and nothing fills one from the other. let scopes: std::collections::BTreeSet = holes.iter().map(|(s, _)| *s).collect(); assert!( scopes.len() >= 3, "expected root and two tables: {scopes:?}" ); } /// A guard no longer takes anything out of the staged render, and that is /// the point rather than a regression. /// /// The twin used to answer each guard from the plan, so a derivation could /// turn one off and measure what went missing. It now places every guarded /// member and marks the run instead, which is why one render answers every /// guard: what a guard controls is written down rather than deduced from a /// gap. `Plan::empty` therefore draws exactly what `Plan::full` draws, and /// the branch is in the marks. #[test] fn a_guard_places_its_emission_and_marks_it_instead() { let webview = Webview::new(); let full = webview.fragment(&pane_staged(&Plan::full(1))); let none = webview.fragment(&pane_staged(&Plan::empty())); assert!(full.contains("Your Buyers ("), "{full}"); assert_eq!( full.matches("table-head").count(), none.matches("table-head").count(), "a guard no longer decides what the twin draws" ); // And the branch it used to make by deletion is in the residual. let residual = quasi_webview::stage::derive(&webview, pane_staged); assert!( residual .ops() .iter() .any(|op| matches!(op, quasi_router::stage::Op::Branch(_))), "{:#?}", residual.ops() ); } /// A loop runs as many times as the plan says, in the scope it is read at. #[test] fn one_loop_grows_without_moving_the_other() { let webview = Webview::new(); let one = webview.fragment(&pane_staged(&Plan::full(1))); let two = webview.fragment(&pane_staged(&Plan::full(2))); assert_eq!(one.matches("table-row").count(), 2); assert_eq!(two.matches("table-row").count(), 4); } } #[cfg(test)] mod residual_tests { use quasi_http::Serves as _; use quasi_router::stage::{Op, Plan, Residual}; use quasi_webview::Webview; use super::*; /// Replay a residual with sentinels still in it, which is what the staged /// twin would have rendered at this many rows. 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), // The base render is the guard-passing side, which is arm 0. Op::Arms(arms) => replay(&arms[0], rows, out), Op::Loop(body) => { for _ in 0..rows { replay(body, rows, out); } } } } } fn residual() -> Residual { quasi_webview::stage::derive(&Webview::new(), pane_staged) } /// The derivation found structure, rather than one flat run of markup. /// The case a branch cannot hold is refused where it is derived. /// /// On the build machine, where a wrong residual is still cheap. The filler /// would not have caught it: it would have served every row the class the /// staged render happened to have. #[test] #[should_panic(expected = "two places at once")] fn a_guarded_cell_that_steers_its_column_is_refused() { let _ = quasi_webview::stage::derive(&Webview::new(), steered_table_staged); } /// A guarded cell empties its column rather than removing it. /// /// The bug this caught, stated as a test: the filled residual has to equal /// the renderer's own output for a row that answers the column and one that /// does not, and the two differ by the contents of a `
` that is /// present either way. #[test] fn a_guarded_cell_leaves_its_column_standing() { let webview = Webview::new(); let residual = quasi_webview::stage::derive(&Webview::new(), sparse_table_staged); let rows = vec![ Buyer { username: "ada".into(), email: String::new(), purchases: String::new(), spent: "$4".into(), last_purchase: String::new(), }, Buyer { username: "bea".into(), email: String::new(), purchases: String::new(), spent: String::new(), last_purchase: String::new(), }, ]; assert_eq!( webview.fragment(&sparse_table(&rows)), sparse_table_serve(&residual, &rows), ); // And the column is still there in both rows, which is the property the // whole split is for. let filled = sparse_table_serve(&residual, &rows); assert_eq!(filled.matches("col-Note").count(), 3, "{filled}"); } #[test] fn the_residual_has_a_branch_and_a_loop_in_it() { let residual = residual(); fn count(ops: &[Op], branches: &mut usize, loops: &mut usize, holes: &mut usize) { for op in ops { match op { Op::Lit(_) => {} Op::Hole { .. } => *holes += 1, Op::Branch(body) => { *branches += 1; count(body, branches, loops, holes); } Op::Arms(arms) => { *branches += 1; for arm in arms.iter() { count(arm, branches, loops, holes); } } Op::Loop(body) => { *loops += 1; count(body, branches, loops, holes); } } } } let (mut branches, mut loops, mut holes) = (0, 0, 0); count(residual.ops(), &mut branches, &mut loops, &mut holes); // Seven guards in the pane and two loops, one per table. assert_eq!(loops, 2, "{:#?}", residual.ops()); assert!(branches >= 6, "branches {branches}: {:#?}", residual.ops()); assert!(holes > 0, "no holes at all"); } /// The residual is the renderer's own output with the repeats marked, so /// replaying it at any row count has to give the twin's render back. #[test] fn replaying_the_residual_reproduces_the_staged_render() { let webview = Webview::new(); let residual = residual(); for rows in [1, 2, 5, 25] { let mut replayed = String::new(); replay(residual.ops(), rows, &mut replayed); assert_eq!( webview.fragment(&pane_staged(&Plan::full(rows))), replayed, "the residual and the renderer disagree at {rows} rows" ); } } } #[cfg(test)] mod filled_tests { use quasi_http::Serves as _; use quasi_router::stage::Residual; use quasi_webview::Webview; use super::*; use crate::fixture::SIZES; fn residual() -> Residual { quasi_webview::stage::derive(&Webview::new(), pane_staged) } /// The measurement this whole path exists for, stated as a test. /// /// Filling the residual builds no `Node`, and what comes out is what the /// renderer produces from the tree. Byte for byte here, which is stronger /// than the acceptance rule asks for: the rule allows markup to move, /// because an emitter may optimise, and this one does not optimise yet. #[test] fn a_filled_residual_is_what_the_renderer_would_have_produced() { let webview = Webview::new(); let residual = residual(); for size in SIZES { let rows = Rows::new(size); assert_eq!( webview.fragment(&describe(&rows)), pane_serve(&residual, &rows.buyers, &rows.shared), "the staged path and the renderer disagree at {size} rows" ); } } /// The branch, which is what the spike could not carry at all. #[test] fn the_empty_screen_goes_through_the_same_residual() { let webview = Webview::new(); let residual = residual(); let rows = Rows::new(0); assert_eq!( webview.fragment(&describe(&rows)), pane_serve(&residual, &rows.buyers, &rows.shared), ); } /// A value that would break the markup is escaped on the way in, through /// the renderer's own escaper rather than a second one. #[test] fn a_hostile_value_is_escaped_the_way_the_renderer_escapes_it() { let webview = Webview::new(); let residual = residual(); let mut rows = Rows::new(3); rows.buyers[1].username = r#""#.into(); rows.shared[0].name = "Bobby & Tables".into(); let filled = pane_serve(&residual, &rows.buyers, &rows.shared); assert_eq!(webview.fragment(&describe(&rows)), filled); assert!(!filled.contains("