Files
wanderbooks/crates/ledger-gui/tests/show.rs
T
swaitsandClaude Opus 5.5 9ab003d0d1
check / check (push) Canceled after 0s
P3: rename accounts, one set of period defaults, tax categories warn or refuse (defects 15, 16, 8)
books.rename_account; account names trimmed, bounded, refused when a
sibling's or TIN-shaped; books.show refuses what its target ignores. A
report asked for without dates covers this fiscal year so far, everywhere
(one rule for tools, descriptions and the viewer; the IIF and CPA package
keep their own dates). policy.require_tax_category warns or refuses; a
Form 4797 gain is shown apart on every return.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
2026-10-21 10:41:24 -06:00

312 lines
12 KiB
Rust

//! books.show requests drive the viewer to the right place.
use egui_kittest::Harness;
use ledger_gui::nav::{ReportKind, Route};
use ledger_gui::{App, Source, data};
use ledger_store::ops::ViewerTarget;
/// 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);
}
}
#[test]
fn show_requests_navigate_the_viewer() {
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
let dir = std::env::temp_dir().join(format!("ledger-gui-show-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let _cleanup = TestDir(dir.clone());
ledger_gui::demo::build(&dir, today).unwrap();
let source = Source {
data_dir: dir.join("books-data"),
today: Some(today),
};
let books = data::load_once(&source).unwrap();
let rent = books.ledger.chart().find_by_code("6200").unwrap().id;
let laptop = books.ledger.asset_by_tag("js-0001").unwrap().id;
let mut h = Harness::builder()
.with_size(egui::vec2(1440.0, 920.0))
.build_eframe(|cc| App::with_books(&cc.egui_ctx, books, true, Route::Overview));
// "Compare this quarter's P&L with the same quarter last year, by month."
h.state_mut().show(ViewerTarget::Report {
report: "profit_and_loss".into(),
start: Some("2026-07-01".into()),
end: Some("2026-09-30".into()),
columns: Some("month".into()),
compare: Some("prior_year".into()),
});
h.run_steps(2);
assert_eq!(
h.state_mut().route(),
&Route::Reports(ReportKind::ProfitAndLoss)
);
let st = &h.state_mut().state_mut().reports;
assert_eq!(
st.range,
ledger_gui::pages::reports::Range::Custom(
chrono::NaiveDate::from_ymd_opt(2026, 7, 1).unwrap(),
chrono::NaiveDate::from_ymd_opt(2026, 9, 30).unwrap()
)
);
assert_eq!(st.columns, ledger_gui::pages::reports::Columns::Month);
// "Show me the rent account for the summer."
h.state_mut().show(ViewerTarget::Account {
code: "6200".into(),
start: Some("2026-06-01".into()),
end: Some("2026-08-31".into()),
});
h.run_steps(2);
assert_eq!(h.state_mut().route(), &Route::Account(rent));
// "Find every Adobe charge."
h.state_mut().show(ViewerTarget::Search {
query: "adobe".into(),
});
h.run_steps(2);
assert_eq!(h.state_mut().route(), &Route::Transactions);
assert_eq!(h.state_mut().state_mut().transactions.query, "adobe");
// "Show me the asset register", "the laptop", "the register at June 30".
h.state_mut().show(ViewerTarget::Page {
page: "assets".into(),
});
h.run_steps(2);
assert_eq!(h.state_mut().route(), &Route::Assets);
h.state_mut().show(ViewerTarget::Asset { asset_id: laptop });
h.run_steps(2);
assert_eq!(h.state_mut().route(), &Route::Asset(laptop));
h.state_mut().show(ViewerTarget::Report {
report: "fixed_asset_register".into(),
start: None,
end: Some("2026-06-30".into()),
columns: None,
compare: None,
});
h.run_steps(2);
assert_eq!(
h.state_mut().route(),
&Route::Reports(ReportKind::FixedAssetRegister)
);
assert!(matches!(
h.state_mut().state_mut().reports.range,
ledger_gui::pages::reports::Range::Custom(_, e)
if e == chrono::NaiveDate::from_ymd_opt(2026, 6, 30).unwrap()
));
// An unknown asset is ignored.
h.state_mut().show(ViewerTarget::Asset { asset_id: 999 });
h.run_steps(1);
assert_eq!(
h.state_mut().route(),
&Route::Reports(ReportKind::FixedAssetRegister)
);
h.state_mut().show(ViewerTarget::Search {
query: "adobe".into(),
});
h.run_steps(2);
// Unknown accounts are ignored, not crashes.
h.state_mut().show(ViewerTarget::Account {
code: "9999".into(),
start: None,
end: None,
});
assert_eq!(h.state_mut().route(), &Route::Transactions);
}
/// The demo books (calendar fiscal year) in a viewer, at `today`.
fn demo(
tag: &str,
today: chrono::NaiveDate,
) -> (TestDir, Harness<'static, App>, ledger_gui::books::Books) {
demo_sized(tag, today, 920.0)
}
/// [`demo`] in a window `height` tall (the register's date row sits below
/// an account's cards: tall enough to click it).
fn demo_sized(
tag: &str,
today: chrono::NaiveDate,
height: f32,
) -> (TestDir, Harness<'static, App>, ledger_gui::books::Books) {
let dir = std::env::temp_dir().join(format!("ledger-gui-show-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let guard = TestDir(dir.clone());
ledger_gui::demo::build(&dir, today).unwrap();
let source = Source {
data_dir: dir.join("books-data"),
today: Some(today),
};
let books = data::load_once(&source).unwrap();
let copy = data::load_once(&source).unwrap();
let h = Harness::builder()
.with_size(egui::vec2(1440.0, height))
.build_eframe(move |cc| App::with_books(&cc.egui_ctx, books, true, Route::Overview));
(guard, h, copy)
}
fn day(y: i32, m: u32, d: u32) -> chrono::NaiveDate {
chrono::NaiveDate::from_ymd_opt(y, m, d).unwrap()
}
/// Defect 15, repros 1.1.1-1.1.2 (rules.md B-30): a target the viewer
/// can't honour is ignored whole, never swapped or guessed: a start after
/// the end (an earlier writer's, or a hand-edited ops/show.jsonl), text
/// that isn't a date, an unknown split or comparison. The page, the
/// report's range and split, and the register's range stay as they were.
#[test]
fn show_never_swaps_an_inverted_target_defect_15() {
use ledger_gui::pages::accounts::RegisterRange;
use ledger_gui::pages::reports::{Columns, Compare, Range};
let (_dir, mut h, _) = demo("inverted", day(2026, 10, 20));
h.state_mut().state_mut().reports.range = Range::LastMonth;
h.state_mut().state_mut().reports.columns = Columns::Quarter;
h.state_mut().state_mut().accounts.range = RegisterRange::All;
let report =
|start: Option<&str>, end: Option<&str>, columns: Option<&str>, compare: Option<&str>| {
ViewerTarget::Report {
report: "profit_and_loss".into(),
start: start.map(Into::into),
end: end.map(Into::into),
columns: columns.map(Into::into),
compare: compare.map(Into::into),
}
};
for target in [
report(Some("2026-12-31"), Some("2026-01-01"), None, None),
report(Some("2027-01-01"), None, None, None),
report(Some("January"), None, None, None),
report(None, Some("2026-13-01"), None, None),
report(None, None, Some("weekly"), None),
report(None, None, None, Some("prior_decade")),
ViewerTarget::Account {
code: "1000".into(),
start: Some("2026-12-31".into()),
end: Some("2026-01-01".into()),
},
ViewerTarget::Account {
code: "1000".into(),
start: Some("2027-01-01".into()),
end: None,
},
] {
h.state_mut().show(target.clone());
h.run_steps(2);
assert_eq!(h.state_mut().route(), &Route::Overview, "{target:?}");
let st = h.state_mut().state_mut();
assert_eq!(st.reports.range, Range::LastMonth, "{target:?}");
assert_eq!(st.reports.columns, Columns::Quarter, "{target:?}");
assert_eq!(st.reports.compare, Compare::None, "{target:?}");
assert_eq!(st.accounts.range, RegisterRange::All, "{target:?}");
}
}
/// Defect 16, repro 1.1.4 (rules.md B-30): a report target with only an
/// end covers that end's fiscal year to it (2025-01-01 to 2025-06-30, not
/// the swapped 2025-06-30 to 2026-01-01); with no dates, the default (the
/// fiscal year to date, named "Year to date"); dates that are a preset's
/// show under its name; an account target with no dates opens the
/// register on the default.
#[test]
fn show_report_with_only_end_uses_that_years_start_defect_16() {
use ledger_gui::pages::accounts::RegisterRange;
use ledger_gui::pages::reports::Range;
let today = day(2026, 10, 20);
let (_dir, mut h, books) = demo("only-end", today);
let report = |start: Option<&str>, end: Option<&str>| ViewerTarget::Report {
report: "profit_and_loss".into(),
start: start.map(Into::into),
end: end.map(Into::into),
columns: None,
compare: None,
};
h.state_mut().show(report(None, Some("2025-06-30")));
h.run_steps(2);
assert_eq!(
h.state_mut().state_mut().reports.range,
Range::Custom(day(2025, 1, 1), day(2025, 6, 30))
);
h.state_mut().state_mut().reports.range = Range::LastMonth;
h.state_mut().show(report(None, None));
h.run_steps(2);
assert_eq!(h.state_mut().state_mut().reports.range, Range::YearToDate);
assert_eq!(Range::YearToDate.dates(&books), (day(2026, 1, 1), today));
// What books.show sends for no dates: the same, by name.
h.state_mut()
.show(report(Some("2026-01-01"), Some("2026-10-20")));
h.run_steps(2);
assert_eq!(h.state_mut().state_mut().reports.range, Range::YearToDate);
h.state_mut()
.show(report(Some("2026-09-01"), Some("2026-09-30")));
h.run_steps(2);
assert_eq!(h.state_mut().state_mut().reports.range, Range::LastMonth);
// Only a start: through today.
h.state_mut().show(report(Some("2026-03-15"), None));
h.run_steps(2);
assert_eq!(
h.state_mut().state_mut().reports.range,
Range::Custom(day(2026, 3, 15), today)
);
// An account with no dates (an earlier writer's): the default.
h.state_mut().state_mut().accounts.range = RegisterRange::All;
h.state_mut().show(ViewerTarget::Account {
code: "1000".into(),
start: None,
end: None,
});
h.run_steps(2);
assert_eq!(
h.state_mut().state_mut().accounts.range,
RegisterRange::Default
);
assert_eq!(
RegisterRange::Default.dates(&books),
(day(2026, 1, 1), today)
);
}
/// Settled in design (P3, defect 16): an account's register opens on the
/// fiscal year to date with "Show all dates" one click away: after
/// books.show of an account (which sends the resolved dates), the button
/// shows every date, the books' first entry through today; from there,
/// "Fiscal year to date" goes back.
#[test]
fn show_all_dates_is_one_click_from_a_shown_register() {
use egui_kittest::kittest::Queryable;
use ledger_gui::pages::accounts::RegisterRange;
let today = day(2026, 10, 20);
let (_dir, mut h, books) = demo_sized("all-dates", today, 3000.0);
let id = books.ledger.chart().find_by_code("1000").unwrap().id;
h.state_mut().show(ViewerTarget::Account {
code: "1000".into(),
start: Some("2026-01-01".into()),
end: Some("2026-10-20".into()),
});
h.run_steps(2);
assert_eq!(h.state_mut().route(), &Route::Account(id));
assert_eq!(
h.state_mut().state_mut().accounts.range,
RegisterRange::Dates(day(2026, 1, 1), today)
);
h.get_by_label("Show all dates").click();
h.run_steps(2);
assert_eq!(h.state_mut().state_mut().accounts.range, RegisterRange::All);
let first = books.kpis.first_entry.unwrap();
assert_eq!(RegisterRange::All.dates(&books), (first.min(today), today));
assert!(
first < day(2026, 1, 1),
"the demo's books began before this year"
);
h.get_by_label("Fiscal year to date").click();
h.run_steps(2);
assert_eq!(
h.state_mut().state_mut().accounts.range,
RegisterRange::Default
);
}