//! Smoke test: every page template loads from the embedded environment and
//! renders with a plausible context. Templates are data, not code, so a typo
//! in one is invisible to `cargo check` — this is what catches it.
use minijinja::{context, Environment};
use fieri_eternal::{add_config_globals, build_environment, Config};
const AREA_ID: &str = "0198f0aa-0000-7000-8000-00000000000a";
const RESTAURANT_ID: &str = "0198f0aa-0000-7000-8000-00000000000b";
fn env() -> Environment<'static> {
let mut env = build_environment();
add_config_globals(&mut env, &Config::from_env());
env
}
fn user() -> minijinja::Value {
context! {
id => "0198f0aa-0000-7000-8000-000000000001",
username => "alice",
email => "alice@example.com",
is_active => true,
created_at => "2026-08-03T00:00:00Z",
last_login_at => "2026-08-03T12:00:00Z",
}
}
fn area() -> minijinja::Value {
context! { id => AREA_ID, name => "Boston office", lat => 42.36, lon => -71.06, zoom => 15 }
}
fn restaurant(archived: bool) -> minijinja::Value {
context! {
id => RESTAURANT_ID,
area_id => AREA_ID,
name => "Tandoor's",
osm_ref => "N1915398676",
lat => 42.361,
lon => -71.059,
cuisine => "indian",
address => "12 Main St",
website => "https://example.com",
notes => "get the lamb",
archived => archived,
my_rank => "positive",
visits => 3,
last_visit => "2026-10-20",
}
}
fn scored(vetoed: bool) -> minijinja::Value {
context! {
id => RESTAURANT_ID,
name => "Tandoor's",
preference => 2.5,
recency => 0.4,
score => 1.0,
probability => 0.42,
last_visit_days => 8,
fans => vec!["alice"],
vetoed_by => if vetoed { vec!["bob"] } else { vec![] },
}
}
/// Every full page renders and carries the app name from `APP_NAME`.
#[test]
fn renders_every_page() {
let env = env();
let cases: Vec<(&str, minijinja::Value)> = vec![
(
"index.html",
context! { title => "Home", user => user(), areas => vec![area()], registration_open => false },
),
("index.html", context! { title => "Home", user => user(), areas => Vec::<i32>::new() }),
("index.html", context! { title => "Home", registration_open => true }),
("auth/login.html", context! { title => "Login" }),
(
"auth/register.html",
context! { title => "Register", invite_token => "tok-123", registration_open => false },
),
("auth/profile.html", context! { title => "Profile", user => user() }),
(
"auth/invites.html",
context! {
title => "Invites",
user => user(),
now => "2026-08-03T00:00:00Z",
invites => vec![context! {
token => "tok-123",
email => "friend@example.com",
expires_at => "2026-09-03T00:00:00Z",
is_used => false,
}],
},
),
(
"areas/list.html",
context! { title => "Areas", user => user(), areas => vec![context! { area => area(), restaurants => 4 }] },
),
("areas/new.html", context! { title => "New area", user => user() }),
(
"areas/show.html",
context! {
title => "Boston office",
user => user(),
area => area(),
restaurants => vec![restaurant(false), restaurant(true)],
markers => vec![context! { name => "Tandoor's", lat => 42.361, lon => -71.059, rank => "always", url => "/r" }],
},
),
(
"restaurants/new.html",
context! { title => "Add", user => user(), area => area(), markers => Vec::<i32>::new() },
),
(
"restaurants/show.html",
context! {
title => "Tandoor's",
user => user(),
restaurant => restaurant(false),
area => area(),
my_rank => "never",
everyone => vec![context! { username => "alice", rank => "never" }, context! { username => "bob" }],
visits => vec![context! { id => "x", went_on => "2026-10-20", attendees => vec!["alice", "bob"] }],
},
),
(
"lunch/plan.html",
context! { title => "Lunch", user => user(), area => area(), people => vec![user()], today => "2026-10-28", max_meals => 14 },
),
(
"lunch/history.html",
context! {
title => "History",
user => user(),
area => area(),
lunches => vec![context! {
id => "x", went_on => "2026-10-20", restaurant_id => RESTAURANT_ID,
restaurant => "Tandoor's", attendees => vec!["alice"],
}],
},
),
];
for (name, ctx) in cases {
let tmpl = env.get_template(name).unwrap_or_else(|e| panic!("load {name}: {e}"));
let html = tmpl.render(ctx).unwrap_or_else(|e| panic!("render {name}: {e}"));
assert!(
html.contains(fieri_eternal::APP_NAME),
"{name} did not render the app name"
);
}
}
/// The empty-collection branches of the list pages.
#[test]
fn renders_empty_lists() {
let env = env();
let pages = [
("auth/invites.html", "invites"),
("areas/list.html", "areas"),
("areas/show.html", "restaurants"),
("lunch/history.html", "lunches"),
];
for (name, key) in pages {
let ctx = context! { user => user(), area => area(), now => "2026-08-03T00:00:00Z", markers => Vec::<i32>::new(), ..
minijinja::Value::from_iter([(key, Vec::<minijinja::Value>::new())]) };
let tmpl = env.get_template(name).unwrap();
tmpl.render(ctx).unwrap_or_else(|e| panic!("render empty {name}: {e}"));
}
}
/// htmx fragments must keep the id their trigger swaps by, or the swap selects
/// nothing and the widget silently disappears.
#[test]
fn fragments_keep_their_swap_ids() {
let env = env();
let rank = env
.get_template("restaurants/_rank.html")
.unwrap()
.render(context! { restaurant_id => RESTAURANT_ID, my_rank => "always" })
.unwrap();
assert!(rank.contains(&format!(r#"id="rank-{RESTAURANT_ID}""#)));
assert!(rank.contains(r#"aria-pressed="true""#));
let candidates = env
.get_template("places/_candidates.html")
.unwrap()
.render(context! {
fit => true,
failed => false,
candidates => vec![context! {
place => context! { name => "O'Brien's", kind => "pub", lat => 1.0, lon => 2.0, distance_m => 40 },
json => context! { name => "O'Brien's", lat => 1.0, lon => 2.0 },
existing_id => RESTAURANT_ID,
}],
})
.unwrap();
assert!(candidates.contains(r#"id="candidates""#));
// The JSON sits in a single-quoted attribute; an apostrophe in a name
// must not end it.
assert!(!candidates.contains("O'Brien's\""), "unescaped quote in data-candidate");
assert!(candidates.contains("already added"));
let plan = env.get_template("lunch/_plan.html").unwrap();
for ctx in [
context! { area => area(), empty_group => true },
context! { area => area(), restaurant_count => 0, going => vec![user()], plan => Vec::<i32>::new() },
context! {
area => area(),
restaurant_count => 2,
going => vec![user()],
plan => vec![context! {
on => "2026-10-28",
pick => scored(false),
restaurant => restaurant(false),
field => vec![scored(false), scored(true)],
}],
},
context! {
area => area(),
restaurant_count => 2,
going => vec![user()],
plan => vec![
context! { on => "2026-10-28", field => vec![scored(true)] },
context! { on => "2026-10-29", pick => scored(false), restaurant => restaurant(false), field => vec![scored(false)] },
],
},
] {
let html = plan.render(ctx).unwrap_or_else(|e| panic!("render plan: {e}"));
assert!(html.contains(r#"id="plan-result""#));
}
}
neiam
/fieri-forever
the flavortown lunch engine
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
8.4 KiB · text
History
25c4237