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 = [(feature.into(), key)].into();
}
fn repo_git_config(ctx: &TestContext) -> PathBuf {
run(
&ctx.dir,
&["git", "config", "delta-tui.visit-context-lines", "true"],
);
ctx.dir.join(".git/config")
}
fn saved_feature_keys(path: &Path) -> Vec<(String, char)> {
let git_config = git2::Config::open(path).unwrap();
crate::config::init_config_from(PathBuf::from("/nonexistent/config.toml"), Some(&git_config))
.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 a_feature_key_turns_off_a_feature_the_users_config_turns_on() {
let mut ctx = setup_clone!();
with_feature_key(&mut ctx, "side-by-side", 'x');
ctx.config().general.diff_renderer.features_in_force = vec!["side-by-side".into()];
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 plus_turns_off_the_plus_minus_markers_the_users_config_turns_on() {
let mut ctx = setup_clone!();
ctx.config().general.diff_renderer.features_in_force = vec!["keep-plus-minus-markers".into()];
let mut app = ctx.init_app();
ctx.update(&mut app, keys("+"));
assert_eq!(&*app.state.features, ["-plus-minus-markers".to_string()]);
}
#[test]
fn the_feature_list_marks_the_features_the_users_config_turns_on() {
let mut ctx = setup_clone!();
ctx.config().general.diff_renderer.features_in_force = vec!["line-numbers".into()];
let mut app = ctx.init_app();
ctx.update(&mut app, keys("-"));
assert!(ctx.redact_buffer().contains("● L line-numbers"));
let mut app = ctx.init_app();
ctx.update(&mut app, keys("L-"));
assert!(ctx.redact_buffer().contains(" L line-numbers"));
}
#[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("gitconfig");
fs::write(
&path,
"[delta-tui \"general.diff-renderer.feature-keys\"]\n\tside-by-side = s\n",
)
.unwrap();
let git_config = git2::Config::open(&path).unwrap();
assert!(
crate::config::init_config_from(dir.path().join("config.toml"), Some(&git_config)).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 path = repo_git_config(&ctx);
let mine = fs::read_to_string(&path).unwrap();
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));
assert_eq!(
saved_feature_keys(&path),
[
("line-numbers".into(), 'L'),
("plus-minus-markers".into(), '+'),
("side-by-side".into(), 'x')
]
);
}
#[test]
fn the_same_letter_again_unsets_the_key() {
let mut ctx = setup_clone!();
let path = repo_git_config(&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),
[
("line-numbers".into(), 'L'),
("plus-minus-markers".into(), '+')
]
);
}
#[test]
fn another_letter_replaces_the_key() {
let mut ctx = setup_clone!();
let path = repo_git_config(&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),
[
("line-numbers".into(), 'L'),
("plus-minus-markers".into(), '+'),
("side-by-side".into(), 'w')
]
);
}
#[test]
fn a_key_set_for_one_feature_is_taken_from_another() {
let mut ctx = setup_clone!();
let path = repo_git_config(&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'),
("plus-minus-markers".into(), '+')
]
);
}
#[test]
fn the_default_features_have_keys_by_default() {
let dir = temp_dir::TempDir::new().unwrap();
let path = dir.path().join("gitconfig");
fs::write(&path, "").unwrap();
assert_eq!(
saved_feature_keys(&path),
[
("line-numbers".into(), 'L'),
("plus-minus-markers".into(), '+'),
("side-by-side".into(), 'S')
]
);
}
#[test]
fn plus_toggles_the_plus_minus_markers_by_default() {
let mut ctx = setup_clone!();
let mut app = ctx.init_app();
ctx.update(&mut app, keys("+"));
assert_eq!(&*app.state.features, ["plus-minus-markers".to_string()]);
}
#[test]
fn a_users_feature_keys_replace_the_default_ones() {
let dir = temp_dir::TempDir::new().unwrap();
let path = dir.path().join("gitconfig");
fs::write(
&path,
"[delta-tui \"general.diff-renderer.feature-keys\"]\n\tside-by-side = x\n",
)
.unwrap();
assert_eq!(saved_feature_keys(&path), [("side-by-side".into(), 'x')]);
}
#[test]
fn a_default_key_unset_in_the_list_stays_unset() {
let mut ctx = setup_clone!();
let path = repo_git_config(&ctx);
let mut app = ctx.init_app();
ctx.update(&mut app, keys("-S<esc>"));
assert_eq!(
saved_feature_keys(&path),
[
("line-numbers".into(), 'L'),
("plus-minus-markers".into(), '+')
]
);
}
#[test]
fn a_letter_bound_at_the_top_level_is_refused() {
let mut ctx = setup_clone!();
let path = repo_git_config(&ctx);
let mine = fs::read_to_string(&path).unwrap();
let mut app = ctx.init_app();
ctx.update(&mut app, keys("-s<esc>"));
assert!(!app.state.feature_keys.values().any(|key| *key == 's'));
assert_eq!(fs::read_to_string(&path).unwrap(), mine);
}
#[test]
fn q_leaves_the_feature_list() {
let mut ctx = setup_clone!();
let path = repo_git_config(&ctx);
let mine = fs::read_to_string(&path).unwrap();
let mut app = ctx.init_app();
ctx.update(&mut app, keys("-q"));
assert!(app.state.picker.is_none(), "the feature list is gone");
assert!(!ctx.redact_buffer().contains("bound already"));
assert_eq!(fs::read_to_string(&path).unwrap(), mine);
}
#[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"]);
}