use bevy_app::App;
use bevy_input_focus::InputFocus;
use plurimus::term::KeyCode;
use super::Sidebar;
use crate::commands::tui::edit::DomainTable;
use crate::commands::tui::edit::tests::open;
use crate::commands::tui::nav::{FocusStop, Group, Page, tab_digit};
use crate::commands::tui::support::{
SIZE, active_page, cell_of, click, commit_edit, composed_frame, headless_app, press_key,
press_shift, redrawn, run_command, show,
};
const HELD_BAR: char = '▌';
fn is_on_sidebar(app: &App) -> bool {
let focused = app.world().resource::<InputFocus>().get();
focused.is_some_and(|entity| app.world().get::<Sidebar>(entity).is_some())
}
fn is_on_form(app: &App) -> bool {
let focused = app.world().resource::<InputFocus>().get();
focused.is_some_and(|entity| app.world().get::<FocusStop>(entity).is_some())
}
fn is_on_table(app: &App) -> bool {
let focused = app.world().resource::<InputFocus>().get();
focused.is_some_and(|entity| app.world().get::<DomainTable>(entity).is_some())
}
fn sidebar_row(app: &App, page: Page) -> String {
let frame = composed_frame(app);
let row = frame.lines().find(|line| {
line.trim_start_matches(['│', HELD_BAR, '▏', ' '])
.starts_with(page.title())
});
row.unwrap_or_else(|| panic!("{page:?}: {frame}"))
.to_owned()
}
#[test]
fn the_sidebar_lists_every_domain_beside_the_one_on_show() {
let mut app = headless_app(SIZE);
press_key(&mut app, KeyCode::Char(tab_digit(Group::Plan.tab())));
let frame = redrawn(&mut app);
for page in Page::ALL.into_iter().filter(|page| page.is_domain()) {
assert!(frame.contains(page.title()), "{page:?}: {frame}");
}
assert!(frame.contains("╭ Plan ") && frame.contains("╭ Accounts "));
show(&mut app, Page::Ledger);
assert!(
!redrawn(&mut app).contains("╭ Plan "),
"and beside nothing else"
);
}
#[test]
fn the_tab_lands_on_the_sidebar_over_the_domain_last_shown() {
let mut app = headless_app(SIZE);
press_key(&mut app, KeyCode::Char(tab_digit(Group::Plan.tab())));
assert_eq!(active_page(&app), Page::Accounts, "the first of them");
assert!(is_on_sidebar(&app));
show(&mut app, Page::Expenses);
assert!(is_on_table(&app), "a domain's own command goes into it");
press_key(&mut app, KeyCode::Char(tab_digit(0)));
press_key(&mut app, KeyCode::Char(tab_digit(Group::Plan.tab())));
assert_eq!(active_page(&app), Page::Expenses);
assert!(is_on_sidebar(&app), "and the tab's digit does not");
assert!(sidebar_row(&app, Page::Expenses).contains(HELD_BAR));
}
#[test]
fn the_tools_tab_lands_on_its_sidebar_over_the_tool_last_shown() {
let mut app = headless_app(SIZE);
press_key(&mut app, KeyCode::Char(tab_digit(Group::Tools.tab())));
assert_eq!(
active_page(&app),
Page::RothConversions,
"the first of them"
);
assert!(is_on_sidebar(&app));
let frame = redrawn(&mut app);
assert!(
frame.contains("╭ Tools ") && frame.contains("↑↓ tool") && !frame.contains("╭ Plan "),
"{frame}"
);
assert!(sidebar_row(&app, Page::RothConversions).contains(HELD_BAR));
press_key(&mut app, KeyCode::Right);
assert!(is_on_form(&app), "the arrow goes into the tool");
press_key(&mut app, KeyCode::Esc);
assert!(is_on_sidebar(&app), "and esc comes back");
assert_eq!(active_page(&app), Page::RothConversions);
run_command(&mut app, "ledger");
run_command(&mut app, "roth-conversions");
assert!(is_on_form(&app), "a tool's own command goes into it");
run_command(&mut app, "tab-previous");
assert_eq!(active_page(&app), Page::Compare);
run_command(&mut app, "tab-next");
assert_eq!(
active_page(&app),
Page::RothConversions,
"the tab remembers"
);
run_command(&mut app, "tab-next");
assert_eq!(
active_page(&app),
Page::Accounts,
"and the plan's tab is next"
);
}
#[test]
fn the_cursor_is_the_page_whichever_of_them_moves() {
let mut app = headless_app(SIZE);
press_key(&mut app, KeyCode::Char(tab_digit(Group::Plan.tab())));
press_key(&mut app, KeyCode::Down);
assert_eq!(
active_page(&app),
Page::Income,
"the page follows the cursor"
);
assert!(is_on_sidebar(&app), "which keeps the keyboard");
assert!(redrawn(&mut app).contains("╭ Income "));
run_command(&mut app, "people");
app.update();
assert!(
sidebar_row(&app, Page::People).contains('▏'),
"and the cursor follows the page: {}",
composed_frame(&app)
);
}
#[test]
fn arrows_and_esc_move_between_the_sidebar_and_the_domain() {
let mut app = headless_app(SIZE);
press_key(&mut app, KeyCode::Char(tab_digit(Group::Plan.tab())));
assert!(redrawn(&mut app).contains("→ into"));
press_key(&mut app, KeyCode::Right);
assert!(is_on_table(&app));
assert!(redrawn(&mut app).contains("← domains"));
press_key(&mut app, KeyCode::Left);
assert!(is_on_sidebar(&app));
press_key(&mut app, KeyCode::Enter);
assert!(is_on_table(&app));
press_key(&mut app, KeyCode::Esc);
assert!(is_on_sidebar(&app));
assert_eq!(active_page(&app), Page::Accounts, "the page never turned");
}
#[test]
fn esc_on_a_page_that_is_not_a_domain_keeps_it() {
let mut app = headless_app(SIZE);
run_command(&mut app, "ledger");
press_key(&mut app, KeyCode::Esc);
press_key(&mut app, KeyCode::Left);
assert_eq!(active_page(&app), Page::Ledger);
assert!(!is_on_sidebar(&app));
}
#[test]
fn a_lone_form_is_never_a_dead_end() {
for page in [Page::Household, Page::Settings] {
let mut app = headless_app(SIZE);
show(&mut app, page);
assert!(redrawn(&mut app).contains("esc domains"), "{page:?}");
press_key(&mut app, KeyCode::Esc);
assert!(is_on_sidebar(&app), "{page:?} hands the keyboard back");
press_key(&mut app, KeyCode::Up);
assert_ne!(active_page(&app), page, "and another domain is a key away");
open(&mut app, page);
press_key(&mut app, KeyCode::Esc);
press_key(&mut app, KeyCode::Esc);
assert!(is_on_sidebar(&app), "{page:?}: out of a field, then out");
}
}
#[test]
fn only_the_list_with_the_keyboard_draws_the_full_bar() {
let mut app = headless_app(SIZE);
press_key(&mut app, KeyCode::Char(tab_digit(Group::Plan.tab())));
let held = |frame: &str| frame.matches(HELD_BAR).count();
let frame = redrawn(&mut app);
assert_eq!(held(&frame), 1, "{frame}");
assert!(sidebar_row(&app, Page::Accounts).contains(HELD_BAR));
press_key(&mut app, KeyCode::Right);
let frame = redrawn(&mut app);
assert_eq!(held(&frame), 1, "{frame}");
assert!(sidebar_row(&app, Page::Accounts).contains('▏'), "{frame}");
}
#[test]
fn the_counts_follow_the_draft() {
let mut app = headless_app(SIZE);
show(&mut app, Page::Accounts);
let before = sidebar_row(&app, Page::Accounts);
commit_edit(&mut app, |plan| {
let spare = plan.accounts[0].clone();
plan.accounts.push(retiretui_engine::plan::Account {
id: "spare".to_owned(),
..spare
});
});
redrawn(&mut app);
let after = sidebar_row(&app, Page::Accounts);
assert!(before.contains(" 2 ") && after.contains(" 3 "), "{after}");
}
#[test]
fn a_lone_form_takes_the_keyboard_as_a_table_does() {
for (page, label) in [
(Page::Household, "Filing status"),
(Page::Settings, "Plan name"),
] {
let mut app = headless_app(SIZE);
show(&mut app, page);
press_key(&mut app, KeyCode::Esc);
assert!(is_on_sidebar(&app));
press_key(&mut app, KeyCode::Right);
assert!(is_on_form(&app), "{page:?}: the arrow goes into it");
press_key(&mut app, KeyCode::Esc);
let (x, y) = cell_of(&app, label);
click(&mut app, x, y);
app.update();
assert!(is_on_form(&app), "{page:?}: and so does a press on it");
}
}
#[test]
fn tab_and_shift_tab_walk_between_the_sidebar_and_the_domain() {
let mut app = headless_app(SIZE);
press_key(&mut app, KeyCode::Char(tab_digit(Group::Plan.tab())));
assert!(
redrawn(&mut app).contains("⇥ pane"),
"the sidebar names the key"
);
press_key(&mut app, KeyCode::Tab);
assert!(is_on_table(&app), "tab goes on to the domain");
press_shift(&mut app, KeyCode::Tab);
assert!(is_on_sidebar(&app), "shift-tab comes back");
press_shift(&mut app, KeyCode::Tab);
assert!(!is_on_sidebar(&app), "and wraps to the page's last pane");
assert_eq!(active_page(&app), Page::Accounts, "the page never turned");
}
#[test]
fn tab_is_quiet_on_a_page_of_one_pane_or_none() {
for command in ["ledger", "overview"] {
let mut app = headless_app(SIZE);
run_command(&mut app, command);
let held = app.world().resource::<InputFocus>().get();
press_key(&mut app, KeyCode::Tab);
press_shift(&mut app, KeyCode::Tab);
let holder = app.world().resource::<InputFocus>().get();
assert_eq!(holder, held, "{command}");
}
}
#[test]
fn a_press_on_the_tab_bar_turns_the_page_and_never_holds_the_keyboard() {
let mut app = headless_app(SIZE);
run_command(&mut app, "ledger");
let (x, y) = cell_of(&app, "Compare");
click(&mut app, x, y);
app.update();
assert_eq!(active_page(&app), Page::Compare);
let focused = app.world().resource::<InputFocus>().get();
assert!(is_on_form(&app), "the page shown has the keyboard");
click(&mut app, x, y);
app.update();
assert_eq!(
app.world().resource::<InputFocus>().get(),
focused,
"and keeps it"
);
}