Files
wanderbooks/crates/ledger-gui/tests/breadcrumbs.rs
T
swaitsandClaude Opus 5.5 321b94562a
check / check (push) Canceled after 0s
Projects: classes as entities, and the sidebar fits short windows
A project is a class with facts (description, status, kind, party,
dates, links, notes) in one project.set event behind the format
feature projects. Four tools (books.add_project, update_project,
projects, project); documents and decisions link to projects; B-37:
on-spec costs are expensed as incurred, repayment is revenue, and the
unrecovered cost is never below zero (Kani). Viewer: a Projects list
and a project page with its figures, by month and by account. The
sidebar measures its footer instead of reserving a fixed 172 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
2026-10-22 03:50:52 -06:00

124 lines
4.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The breadcrumb links back up: every crumb but the page you're on goes
//! where it names.
//!
//! ```text
//! GOAL: from Parties › Tobin Harlow, clicking "Parties" goes back to the
//! directory; deeper trails link each level.
//! SOURCE: DECISION (owner, 2026-10-16: "I should be able to click
//! parties and it takes me back there ... when it goes deeper").
//! INVARIANTS: palette::crumbs 1-3, and
//! 4. Clicking a crumb goes to its route (a history step, so Back
//! returns).
//! ```
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use ledger_gui::nav::{ReportKind, Route, Section};
use ledger_gui::palette::{crumbs, describe};
use ledger_gui::{App, Source, data};
/// A test's dir, removed when the test ends, pass or panic.
struct TestDir(std::path::PathBuf);
impl Drop for TestDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
/// The demo books, kept while the guard lives.
fn demo(tag: &str) -> (TestDir, Source) {
let dir = std::env::temp_dir().join(format!("ledger-gui-crumbs-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
ledger_gui::demo::build(&dir, today).unwrap();
let source = Source {
data_dir: dir.join("books-data"),
today: Some(today),
};
(TestDir(dir), source)
}
/// INVARIANTS 1-3 on every kind of page in the demo books.
#[test]
fn crumbs_link_every_level_but_the_last() {
let (_dir, source) = demo("unit");
let b = data::load_once(&source).unwrap();
let party = b.ledger.parties().next().unwrap().id;
let rec = b.reconciliations.first().expect("the demo reconciles");
let entry = b.entries.first().unwrap().id;
let mut routes = vec![
Route::Party(party),
Route::Project(b.ledger.list_classes()[0].id),
Route::Reconciliation(rec.id),
Route::Entry(entry),
Route::Reports(ReportKind::ProfitAndLoss),
Route::Reports(ReportKind::BalanceSheet),
];
routes.extend(Section::ALL.iter().map(|s| s.home()));
for r in &routes {
let c = crumbs(Some(&b), r);
assert_eq!(c[0].0, r.section().title(), "{r:?}");
assert!(c.last().unwrap().1.is_none(), "{r:?}: the page you're on");
for (_, to) in &c {
assert_ne!(to.as_ref(), Some(r), "{r:?} links to itself");
}
// Every crumb above is a link, except one naming this very page
// (Reports on Profit and loss, the Reports section's home).
for (i, (_, to)) in c[..c.len() - 1].iter().enumerate() {
let home_here = i == 0 && r.section().home() == *r;
assert_eq!(to.is_some(), !home_here, "{r:?}: crumb {i}");
}
let here = describe(&b, r);
if !here.is_empty() {
assert_eq!(c.last().unwrap().0, here);
}
}
assert_eq!(
crumbs(Some(&b), &Route::Party(party)),
[
("Parties".to_string(), Some(Route::Parties)),
(b.party_label(party), None),
]
);
// A reconciliation sits under its account.
let account = Route::Account(rec.account_id);
let c = crumbs(Some(&b), &Route::Reconciliation(rec.id));
assert_eq!(c.len(), 3);
assert_eq!(c[0].1, Some(Route::Accounts));
assert_eq!(c[1], (describe(&b, &account), Some(account)));
// A section's own page is one crumb, not a link.
assert_eq!(
crumbs(Some(&b), &Route::Parties),
[("Parties".to_string(), None)]
);
// P&L is the Reports section's home, so "Reports" there isn't a link.
assert_eq!(
crumbs(Some(&b), &Route::Reports(ReportKind::ProfitAndLoss))[0].1,
None
);
}
/// INVARIANT 4, through the top bar.
#[test]
fn clicking_a_crumb_goes_there() {
let (_dir, source) = demo("click");
let b = data::load_once(&source).unwrap();
let rec = b.reconciliations.first().unwrap().clone();
let account = describe(&b, &Route::Account(rec.account_id));
let mut h = Harness::builder()
.with_size(egui::vec2(1280.0, 900.0))
.build_eframe(|cc| App::with_books(&cc.egui_ctx, b, false, Route::Reconciliation(rec.id)));
h.run_steps(3);
h.get_all_by_label(&account).next().unwrap().click();
h.run_steps(3);
assert_eq!(h.state().route(), &Route::Account(rec.account_id));
// The crumb, not the sidebar's link of the same name (left of it).
h.get_all_by_label("Accounts")
.max_by(|a, b| a.rect().left().total_cmp(&b.rect().left()))
.unwrap()
.click();
h.run_steps(3);
assert_eq!(h.state().route(), &Route::Accounts);
}