//! 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("