use super::*;
use std::path::{Path, PathBuf};
fn setup_scroll(mut ctx: TestContext) -> (TestContext, crate::app::App) {
for file in ["file-1", "file-2", "file-3"] {
commit(&ctx.dir, file, "");
fs::write(
ctx.dir.join(file),
(1..=20).fold(String::new(), |mut acc, i| {
use std::fmt::Write as _;
writeln!(acc, "line {} ({})", i, file).unwrap();
acc
}),
)
.unwrap();
}
let mut app = ctx.init_app();
ctx.update(&mut app, keys("jjjj<tab>k<tab>k<tab>"));
(ctx, app)
}
#[test]
fn scroll_down() {
let (mut ctx, mut app) = setup_scroll(setup_clone!());
ctx.update(&mut app, keys(HALF_PAGE_DOWN));
insta::assert_snapshot!(ctx.redact_buffer());
}
#[test]
fn folding_everything_and_opening_it_again() {
let (mut ctx, mut app) = setup_scroll(setup_clone!());
ctx.update(&mut app, keys("<backtab>"));
let folded = ctx.redact_buffer();
ctx.update(&mut app, keys("<backtab>"));
let opened = ctx.redact_buffer();
assert!(!folded.contains("line 1 (file-1)"), "still open:\n{folded}");
assert!(folded.contains("file-1"), "{folded}");
assert!(
opened.contains("line 1 (file-1)"),
"still folded:\n{opened}"
);
}
#[test]
fn folding_everything_is_bound_to_shift_tab() {
let (mut ctx, mut app) = setup_scroll(setup_clone!());
ctx.update(&mut app, keys("<shift+backtab>"));
assert!(
!ctx.redact_buffer().contains("line 1 (file-1)"),
"shift+tab did nothing:\n{}",
ctx.redact_buffer()
);
ctx.update(&mut app, keys("h"));
assert!(
ctx.redact_buffer().contains("shift+tab"),
"{}",
ctx.redact_buffer()
);
}
#[test]
fn shift_tab_reaches_a_backtab_binding() {
let (mut ctx, mut app) = setup_scroll(setup_clone!());
ctx.update(&mut app, keys("<shift+backtab>"));
assert!(
!ctx.redact_buffer().contains("line 1 (file-1)"),
"shift-tab did nothing:\n{}",
ctx.redact_buffer()
);
}
#[test]
fn a_page_is_a_whole_viewport() {
let (mut halves, mut halves_app) = setup_scroll(TestContext::setup_clone("page_in_halves"));
halves.update(&mut halves_app, keys("<ctrl+d><ctrl+d>"));
let (mut page, mut page_app) = setup_scroll(TestContext::setup_clone("page_at_once"));
page.update(&mut page_app, keys("<pagedown>"));
assert_eq!(page.redact_buffer(), halves.redact_buffer());
}
#[test]
fn space_turns_the_page() {
let (mut keys_ctx, mut keys_app) = setup_scroll(TestContext::setup_clone("page_by_key"));
keys_ctx.update(&mut keys_app, keys("<pagedown><pagedown><pageup>"));
let (mut space_ctx, mut space_app) = setup_scroll(TestContext::setup_clone("page_by_space"));
space_ctx.update(&mut space_app, keys("<space><space><backspace>"));
assert_eq!(space_ctx.redact_buffer(), keys_ctx.redact_buffer());
}
#[test]
fn a_page_down_and_a_page_up_come_back() {
let (mut ctx, mut app) = setup_scroll(setup_clone!());
let before = ctx.redact_buffer();
ctx.update(&mut app, keys("<pagedown>"));
let paged = ctx.redact_buffer();
ctx.update(&mut app, keys("<pageup>"));
assert_ne!(paged, before, "page down did not scroll");
assert_eq!(ctx.redact_buffer(), before, "page up did not come back");
}
#[test]
fn paging_carries_the_cursor_into_the_hunk() {
let (mut ctx, mut app) = setup_scroll(setup_clone!());
ctx.update(
&mut app,
keys(&format!("{HALF_PAGE_DOWN}{HALF_PAGE_DOWN}{HALF_PAGE_DOWN}")),
);
insta::assert_snapshot!(ctx.redact_buffer());
}
#[test]
fn move_prev_sibling() {
let (mut ctx, mut app) = setup_scroll(setup_clone!());
ctx.update(&mut app, keys("<alt+k><alt+k>"));
insta::assert_snapshot!(ctx.redact_buffer());
}
#[test]
fn move_next_sibling() {
let (mut ctx, mut app) = setup_scroll(setup_clone!());
ctx.update(&mut app, keys("<alt+j>"));
insta::assert_snapshot!(ctx.redact_buffer());
}
#[test]
fn move_next_then_parent_section() {
let (mut ctx, mut app) = setup_scroll(setup_clone!());
ctx.update(&mut app, keys("<alt+j><alt+h>"));
insta::assert_snapshot!(ctx.redact_buffer());
}
#[test]
fn exit_from_picker_exits_menu() {
snapshot!(setup_clone!(), "bb<esc>");
}
#[test]
fn re_enter_picker_from_menu() {
snapshot!(setup_clone!(), "bb<esc>bb");
}
#[test]
fn renderer_features_offers_the_configured_ones() {
snapshot!(setup_clone!(), "-");
}
fn with_feature_key(ctx: &mut TestContext, feature: &str, key: char) {
ctx.config()
.general
.diff_renderer
.feature_keys
.insert(feature.into(), key);
}
fn config_file(ctx: &mut TestContext, text: &str) -> (temp_dir::TempDir, PathBuf) {
let dir = temp_dir::TempDir::new().unwrap();
let path = dir.path().join("config.toml");
if !text.is_empty() {
fs::write(&path, text).unwrap();
}
ctx.config().path = path.clone();
(dir, path)
}
fn saved_feature_keys(path: &Path) -> Vec<(String, char)> {
crate::config::init_config(Some(path.to_owned()))
.unwrap()
.general
.diff_renderer
.feature_keys
.into_iter()
.collect()
}
#[test]
fn a_feature_key_toggles_its_feature() {
let mut ctx = setup_clone!();
with_feature_key(&mut ctx, "side-by-side", 'x');
let mut app = ctx.init_app();
ctx.update(&mut app, keys("x"));
assert_eq!(&*app.state.features, ["side-by-side".to_string()]);
ctx.update(&mut app, keys("x"));
assert!(app.state.features.is_empty());
}
#[test]
fn the_help_menu_lists_feature_keys() {
let mut ctx = setup_clone!();
with_feature_key(&mut ctx, "side-by-side", 'x');
let mut app = ctx.init_app();
ctx.update(&mut app, keys("?"));
let buffer = ctx.redact_buffer();
assert!(
regex::Regex::new(r"\bx +side-by-side")
.unwrap()
.is_match(&buffer),
"{buffer}"
);
}
#[test]
fn the_help_menu_sets_feature_keys_apart_from_submenus() {
let mut ctx = setup_clone!();
with_feature_key(&mut ctx, "side-by-side", 'x');
snapshot!(ctx, "?");
}
#[test]
fn a_configured_feature_key_that_is_already_bound_is_a_config_error() {
let dir = temp_dir::TempDir::new().unwrap();
let path = dir.path().join("config.toml");
fs::write(
&path,
"[general]\ndiff_renderer.feature_keys = { side-by-side = \"s\" }\n",
)
.unwrap();
assert!(crate::config::init_config(Some(path)).is_err());
}
#[test]
fn the_feature_list_is_moved_through_with_the_arrows() {
let mut ctx = setup_clone!();
let mut app = ctx.init_app();
ctx.update(&mut app, keys("<-><down><enter>"));
assert_eq!(&*app.state.features, ["line-numbers".to_string()]);
}
#[test]
fn the_feature_list_shows_feature_keys() {
let mut ctx = setup_clone!();
with_feature_key(&mut ctx, "line-numbers", 'x');
snapshot!(ctx, "-");
}
#[test]
fn a_letter_in_the_feature_list_sets_the_selected_features_key() {
let mut ctx = setup_clone!();
let (_dir, path) = config_file(&mut ctx, "# mine\n[general]\nvisit_context_lines = true\n");
let mut app = ctx.init_app();
ctx.update(&mut app, keys("-x<esc>"));
assert!(
app.state.features.is_empty(),
"setting a key toggles nothing"
);
ctx.update(&mut app, keys("x"));
assert_eq!(&*app.state.features, ["side-by-side".to_string()]);
assert!(fs::read_to_string(&path).unwrap().starts_with("# mine\n"));
assert_eq!(saved_feature_keys(&path), [("side-by-side".into(), 'x')]);
}
#[test]
fn the_same_letter_again_unsets_the_key() {
let mut ctx = setup_clone!();
let (_dir, path) = config_file(&mut ctx, "");
let mut app = ctx.init_app();
ctx.update(&mut app, keys("-xx<esc>x"));
assert!(app.state.features.is_empty());
assert_eq!(saved_feature_keys(&path), []);
}
#[test]
fn another_letter_replaces_the_key() {
let mut ctx = setup_clone!();
let (_dir, path) = config_file(&mut ctx, "");
let mut app = ctx.init_app();
ctx.update(&mut app, keys("-xw<esc>x"));
assert!(app.state.features.is_empty());
ctx.update(&mut app, keys("w"));
assert_eq!(&*app.state.features, ["side-by-side".to_string()]);
assert_eq!(saved_feature_keys(&path), [("side-by-side".into(), 'w')]);
}
#[test]
fn a_key_set_for_one_feature_is_taken_from_another() {
let mut ctx = setup_clone!();
let (_dir, path) = config_file(&mut ctx, "");
let mut app = ctx.init_app();
ctx.update(&mut app, keys("-x<down>x<esc>x"));
assert_eq!(&*app.state.features, ["line-numbers".to_string()]);
assert_eq!(saved_feature_keys(&path), [("line-numbers".into(), 'x')]);
}
#[test]
fn a_letter_bound_at_the_top_level_is_refused() {
let mut ctx = setup_clone!();
let (_dir, path) = config_file(&mut ctx, "");
let mut app = ctx.init_app();
ctx.update(&mut app, keys("-s<esc>"));
assert!(app.state.feature_keys.is_empty());
assert!(!path.exists());
}
#[test]
fn renderer_features_offers_those_in_git_config_a_pattern_matches() {
let mut ctx = setup_clone!();
ctx.config().general.diff_renderer.features = vec!["*".into()];
run(
&ctx.dir,
&["git", "config", "delta.my-theme.syntax-theme", "Nord"],
);
let mut app = ctx.init_app();
ctx.update(&mut app, keys("<-><enter>"));
assert_eq!(&*app.state.features, ["my-theme".to_string()]);
}
#[test]
fn choosing_a_renderer_feature_marks_it() {
let ctx = setup_clone!();
let mut ctx = ctx;
let mut app = ctx.init_app();
ctx.update(&mut app, keys("<-><enter>"));
assert_eq!(&*app.state.features, ["side-by-side".to_string()]);
ctx.update(&mut app, keys("<-><enter>"));
assert!(
app.state.features.is_empty(),
"picking it again turns it off"
);
}
#[test]
fn suspending_is_bound_to_ctrl_z() {
let mut ctx = setup_clone!();
let ctrl_z = [(
crossterm::event::KeyModifiers::CONTROL,
crossterm::event::KeyCode::Char('z'),
)];
let ops: Vec<&str> = ctx
.config()
.bindings
.match_bindings(&crate::menu::Menu::Root, &ctrl_z)
.map(|binding| binding.op.as_ref())
.collect();
assert_eq!(ops, ["suspend"]);
}