mod common;
use common::{get, sandbox, start};
const ALL_DAILY: &str = "tool=pi&bucket=daily&window=all";
#[tokio::test]
async fn health_and_static_assets() {
let addr = start().await;
let h = get(addr, "/api/health").await;
assert_eq!(h.status, 200);
assert_eq!(h.body.trim(), "\"ok\"");
let css = get(addr, "/style.css").await;
assert_eq!(css.status, 200);
assert!(css.headers.contains("text/css"));
assert!(css.body.contains(".charts"), "chart styles are served");
assert_eq!(get(addr, "/nope").await.status, 404);
}
#[tokio::test]
async fn the_dashboard_renders_summary_charts_and_table() {
let addr = start().await;
let r = get(addr, &format!("/?{ALL_DAILY}")).await;
assert_eq!(r.status, 200);
assert!(r.headers.contains("text/html"));
let html = r.body;
assert!(html.contains("TokenBurn"));
assert!(
html.contains("http-equiv=\"refresh\""),
"static mode reloads itself"
);
assert!(html.contains(">3<") || html.contains("3\n"), "turn count");
assert_eq!(
html.matches("<svg class=\"chart").count(),
2,
"bars + donut"
);
assert!(html.contains("Tokens over time"));
assert!(html.contains("Share by project"));
assert!(html.contains("<rect"), "bar segments");
assert!(html.contains("<title>"), "hover tooltips");
assert!(html.contains("2026-05-01") && html.contains("2026-05-02"));
assert!(html.contains("demo-proj") || html.contains("pi"));
}
#[tokio::test]
async fn the_chart_metric_switches_with_the_query() {
let addr = start().await;
let r = get(addr, &format!("/?{ALL_DAILY}&metric=cost")).await;
assert!(r.body.contains("Cost over time"));
assert!(r.body.contains("selected") && r.body.contains("cost"));
let r = get(addr, &format!("/?{ALL_DAILY}&metric=garbage")).await;
assert!(
r.body.contains("Tokens over time"),
"unknown metric → tokens"
);
}
#[tokio::test]
async fn an_empty_selection_says_so_instead_of_breaking() {
let addr = start().await;
let r = get(addr, "/?tool=amp&bucket=daily&window=all").await;
assert_eq!(r.status, 200);
assert!(r.body.contains("No data"), "empty charts show a message");
}
#[tokio::test]
async fn untrusted_query_values_are_ignored_and_never_reflected() {
let addr = start().await;
let r = get(
addr,
"/?tool=%3Cscript%3Ealert(1)%3C/script%3E&bucket=x&window=y",
)
.await;
assert_eq!(r.status, 200);
assert!(!r.body.contains("<script>alert"), "no reflected markup");
}
#[tokio::test]
async fn the_json_api_returns_the_same_report() {
let addr = start().await;
let r = get(addr, &format!("/api/report?{ALL_DAILY}")).await;
assert_eq!(r.status, 200);
assert!(r.headers.contains("application/json"));
let v: serde_json::Value = serde_json::from_str(&r.body).unwrap();
let rows = v.as_array().unwrap();
assert_eq!(rows.len(), 2, "two days");
let total: u64 = rows
.iter()
.map(|r| r["total_tokens"].as_u64().unwrap())
.sum();
assert_eq!(total, 1000 + 500 + 2000 + 1000 + 3000 + 1500);
}
static THEME_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn theme_guard() -> std::sync::MutexGuard<'static, ()> {
THEME_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn themes_are_listed_and_a_chosen_one_is_applied_and_remembered() {
let _guard = theme_guard();
let addr = start().await;
let t = get(addr, "/api/themes").await;
let v: serde_json::Value = serde_json::from_str(&t.body).unwrap();
let n = v
.as_array()
.map_or_else(|| v["themes"].as_array().unwrap().len(), Vec::len);
assert_eq!(n, tokenburn_core::THEME_COUNT);
let r = get(addr, "/?theme=Dracula&window=all").await;
assert!(r.body.contains("--s0"), "theme variables are inlined");
let saved = std::fs::read_to_string(sandbox().config.join("settings.json")).unwrap();
assert!(saved.contains("Dracula"), "{saved}");
let r = get(addr, "/?theme=Nope&window=all").await;
assert_eq!(r.status, 200);
let saved = std::fs::read_to_string(sandbox().config.join("settings.json")).unwrap();
assert!(saved.contains("Dracula"));
}
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn the_system_theme_follows_the_browser_and_is_remembered() {
let _guard = theme_guard();
let addr = start().await;
let r = get(addr, "/?theme=System&window=all").await;
assert_eq!(r.status, 200);
assert!(
r.body.contains("prefers-color-scheme:light"),
"dark base plus a light media override"
);
assert!(
r.body.contains("<option value=\"System\""),
"listed in the select"
);
let saved = std::fs::read_to_string(sandbox().config.join("settings.json")).unwrap();
assert!(saved.contains("\"System\""), "{saved}");
let r = get(addr, "/?window=all").await;
assert!(r.body.contains("prefers-color-scheme:light"));
}
fn first_link(html: &str, kind: &str) -> String {
let needle = format!("sel_{kind}=");
let at = html
.find(&needle)
.unwrap_or_else(|| panic!("no {needle} link in the page"));
let start = html[..at].rfind("href=\"").unwrap() + 6;
let end = at + html[at..].find('"').unwrap();
html[start..end].replace("&", "&")
}
#[tokio::test]
async fn bars_and_slices_are_links_in_static_mode() {
let addr = start().await;
let html = get(addr, &format!("/?{ALL_DAILY}")).await.body;
let bar = first_link(&html, "bucket");
assert!(
bar.starts_with("/?tool=pi&bucket=daily&window=all&metric=tokens&sel_bucket="),
"{bar}"
);
let slice = first_link(&html, "project");
assert!(slice.contains("sel_project=pi%3A"), "{slice}");
assert!(html.contains("click a bar or a slice to drill down"));
assert!(!html.contains("class=\"col sel"), "nothing selected yet");
}
#[tokio::test]
async fn clicking_a_bar_scopes_the_page_to_that_bucket_and_the_link_toggles_it_off() {
let addr = start().await;
let all = get(addr, &format!("/?{ALL_DAILY}")).await.body;
let bar = first_link(&all, "bucket");
assert!(bar.contains("sel_bucket=2026-05-01"), "{bar}");
let r = get(addr, &bar).await;
assert_eq!(r.status, 200);
let html = r.body;
assert!(html.contains("col sel"), "the clicked bar is marked");
assert!(
html.contains("bars chart") || html.contains("has-sel"),
"the others dim"
);
assert!(html.contains("click to clear"));
assert!(html.contains("class=\"chip\"") && html.contains("2026-05-01"));
let table = html.split("<table").nth(1).unwrap();
assert!(
table.contains("2026-05-01") && !table.contains("2026-05-02"),
"table scoped"
);
assert!(html.contains("sel_bucket=2026-05-02"));
let again = first_link(&html, "bucket");
assert!(!again.is_empty());
let clear_href = {
let at = html.find("class=\"clear\"").unwrap();
let start = html[at..].find("href=\"").unwrap() + at + 6;
let end = start + html[start..].find('"').unwrap();
html[start..end].replace("&", "&")
};
assert!(!clear_href.contains("sel_"), "{clear_href}");
}
#[tokio::test]
async fn clicking_a_slice_scopes_to_that_project() {
let addr = start().await;
let all = get(addr, &format!("/?{ALL_DAILY}")).await.body;
let slice = first_link(&all, "project");
let r = get(addr, &slice).await.body;
assert!(r.contains("slice sel"), "the clicked slice is marked");
assert!(r.contains("click to clear"));
assert!(r.contains("legend vertical has-sel"));
assert!(r.contains("class=\"item sel\""));
}
#[tokio::test]
async fn both_selections_combine_and_junk_selections_are_ignored() {
let addr = start().await;
let r = get(
addr,
&format!("/?{ALL_DAILY}&sel_bucket=2026-05-02&sel_project=pi%3Anope"),
)
.await;
assert_eq!(r.status, 200);
assert_eq!(
r.body.matches("class=\"chip\"").count(),
1,
"only the real bucket is kept"
);
assert!(
!r.body.contains("pi:nope"),
"the unknown project is dropped, not echoed"
);
let r = get(addr, &format!("/?{ALL_DAILY}&sel_bucket=%3Cscript%3E")).await;
assert!(!r.body.contains("<script>"), "never reflected");
assert!(
!r.body.contains("class=\"chip\""),
"an unknown bucket is dropped"
);
}
#[tokio::test]
async fn there_is_no_manual_refresh_control() {
let addr = start().await;
let html = get(addr, &format!("/?{ALL_DAILY}")).await.body;
assert!(!html.contains(">Refresh<"));
}
#[tokio::test]
async fn tables_scroll_sideways_and_secondary_columns_are_tagged_for_the_media_queries() {
let addr = start().await;
let html = get(addr, &format!("/?{ALL_DAILY}")).await.body;
assert!(
html.contains("class=\"table-wrap\""),
"tables are wrapped so they never overflow the page"
);
assert!(html.contains("<th class=\"opt1\">cache read</th>"));
assert!(html.contains("<th class=\"opt2\">input</th>"));
assert!(html.contains("<th>total</th>") && html.contains("<th>cost</th>"));
assert!(
html.contains("name=\"viewport\""),
"the viewport meta makes the media queries work on phones"
);
assert!(html.contains("class=\"axis alt\"") || html.matches("class=\"axis\"").count() > 0);
}
#[tokio::test]
async fn title_shows_the_version_and_about_and_settings_pages_exist() {
let addr = start().await;
let v = tokenburn_core::appinfo::version_label();
let home = get(addr, "/?tool=all&bucket=daily&window=all").await;
assert!(home.body.contains(&v), "version missing on the dashboard");
let about = get(addr, "/about").await;
assert_eq!(about.status, 200);
assert!(about.body.contains("Developer") && about.body.contains(&v));
let settings = get(addr, "/settings").await;
assert_eq!(settings.status, 200);
assert!(settings.body.contains("name=\"theme\""));
}