quinjet 0.0.35

A fast, live, keyboard-first Git source-control interface for the terminal
use super::*;

#[test]
fn an_existing_expand_all_preference_loads_every_file_in_a_new_index() {
    let mut app = App::new("/tmp/repo", "repo");
    app.diff_generation = 9;
    app.files_collapsed = false;
    app.collapse_preference_set = true;

    let effects = app.handle_worker_event(
        WorkerEvent::LocalDiffIndex {
            generation: 9,
            result: Ok(DiffIndex {
                title: "Next commit".to_owned(),
                files: ["one.rs", "two.rs", "three.rs"]
                    .into_iter()
                    .map(|path| crate::git::diff::DiffFileIndexEntry {
                        path: PathBuf::from(path),
                        old_path: None,
                        status: "modified".to_owned(),
                        counts: None,
                    })
                    .collect(),
                truncated: false,
                commit_details: None,
            }),
        },
        Instant::now(),
    );

    assert!(matches!(
        effects.as_slice(),
        [AppEffect::Git(command)] if matches!(
            command.as_ref(),
            WorkerCommand::LoadLocalDiffFile { path, .. } if path == Path::new("one.rs")
        )
    ));
    assert_eq!(
        app.local_diff_pending_paths,
        VecDeque::from([PathBuf::from("two.rs"), PathBuf::from("three.rs")])
    );
    assert!(
        app.preview_file_paths()
            .iter()
            .all(|path| !app.preview_file_collapsed(&path.to_string_lossy()))
    );
}

#[test]
fn local_diff_file_replies_must_match_the_prepared_workspace() {
    let mut app = App::new("/tmp/repo", "repo");
    app.diff_generation = 12;
    app.handle_worker_event(
        WorkerEvent::LocalDiffIndex {
            generation: 12,
            result: Ok(DiffIndex {
                title: "Current".to_owned(),
                files: vec![crate::git::diff::DiffFileIndexEntry {
                    path: PathBuf::from("same.rs"),
                    old_path: None,
                    status: "modified".to_owned(),
                    counts: None,
                }],
                truncated: false,
                commit_details: None,
            }),
        },
        Instant::now(),
    );

    let effects = app.handle_worker_event(
        WorkerEvent::LocalDiffFile {
            generation: 12,
            workspace_generation: 11,
            path: PathBuf::from("same.rs"),
            result: Ok(DiffDocument::empty("Old commit", "stale")),
        },
        Instant::now(),
    );

    assert!(effects.is_empty());
    assert_eq!(app.local_diff_loading_path, Some(PathBuf::from("same.rs")));
    assert!(!app.local_diff_single_loaded);
    assert_ne!(app.document.title, "Old commit");
}

#[test]
fn local_diff_failure_replaces_the_loading_placeholder() {
    let mut app = App::new("/tmp/repo", "repo");
    app.diff_generation = 12;
    app.handle_worker_event(
        WorkerEvent::LocalDiffIndex {
            generation: 12,
            result: Ok(DiffIndex {
                title: "Stash".to_owned(),
                files: vec![crate::git::diff::DiffFileIndexEntry {
                    path: PathBuf::from("index2.ts"),
                    old_path: None,
                    status: "added".to_owned(),
                    counts: None,
                }],
                truncated: false,
                commit_details: None,
            }),
        },
        Instant::now(),
    );

    app.handle_worker_event(
        WorkerEvent::LocalDiffFile {
            generation: 12,
            workspace_generation: 12,
            path: PathBuf::from("index2.ts"),
            result: Err("missing stash parent".to_owned()),
        },
        Instant::now(),
    );

    assert!(app.local_diff_single_loaded);
    assert!(
        app.document
            .lines
            .iter()
            .any(|line| line.text() == "Unable to load diff: missing stash parent")
    );
}

#[test]
fn pull_request_folders_are_selectable_and_collapsible() {
    let mut app = App::new("/tmp/repo", "repo");
    app.view = View::PullRequests;
    app.pull_request_section = PullRequestSection::Files;
    app.focus = Focus::Sidebar;
    app.pull_request_files = ["src/app.rs", "src/git/diff.rs", "tests/ui.rs"]
        .into_iter()
        .map(|path| PullRequestFile {
            path: PathBuf::from(path),
            old_path: None,
            status: PullRequestFileStatus::Modified,
            counts: None,
        })
        .collect();
    app.sync_pull_request_tree_cursor_to_file();

    let cursor = app.pull_request_tree_cursor;
    let entries = app.pull_request_tree_entries();
    assert!(matches!(
        entries.get(cursor),
        Some(PullRequestTreeEntry::File { index: 0, .. })
    ));

    app.handle_key(
        KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
        Instant::now(),
    );
    let cursor = app.pull_request_tree_cursor;
    assert!(matches!(
        app.pull_request_tree_entries().get(cursor),
        Some(PullRequestTreeEntry::Directory { path, .. }) if path == Path::new("src")
    ));

    app.handle_key(
        KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
        Instant::now(),
    );
    assert_eq!(app.focus, Focus::Content);
    assert!(!app.pull_request_directory_collapsed(Path::new("src")));

    app.handle_key(
        KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
        Instant::now(),
    );
    app.handle_key(
        KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
        Instant::now(),
    );
    assert!(app.pull_request_directory_collapsed(Path::new("src")));
    assert_eq!(
        app.pull_request_tree_entries()
            .iter()
            .filter(|entry| matches!(entry, PullRequestTreeEntry::File { .. }))
            .count(),
        1
    );

    app.handle_key(
        KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
        Instant::now(),
    );
    assert!(app.pull_request_directory_collapsed(Path::new("src")));

    app.handle_key(
        KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
        Instant::now(),
    );
    assert!(!app.pull_request_directory_collapsed(Path::new("src")));
    assert_eq!(
        app.pull_request_tree_entries()
            .iter()
            .filter(|entry| matches!(entry, PullRequestTreeEntry::File { .. }))
            .count(),
        3
    );

    app.geometry.sidebar = Rect::new(0, 0, 40, 10);
    app.geometry.sidebar_hits = vec![SidebarHitArea {
        area: Rect::new(0, 2, 40, 1),
        target: SidebarHit::PullRequestDirectory(PathBuf::from("src/git")),
    }];
    app.handle_mouse(
        MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column: 5,
            row: 2,
            modifiers: KeyModifiers::SHIFT,
        },
        Instant::now(),
    );
    assert!(!app.pull_request_directory_collapsed(Path::new("src/git")));

    app.handle_mouse(
        MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column: 5,
            row: 2,
            modifiers: KeyModifiers::NONE,
        },
        Instant::now(),
    );
    assert!(app.pull_request_directory_collapsed(Path::new("src/git")));
    assert!(
        !app.pull_request_tree_entries()
            .iter()
            .any(|entry| { matches!(entry, PullRequestTreeEntry::File { index: 1, .. }) })
    );
}

#[test]
fn pull_request_tree_compacts_single_child_directory_chains() {
    let mut app = App::new("/tmp/repo", "repo");
    app.pull_request_files = [
        "apps/web/app/one.txt",
        "apps/web/modules/marketing/landing/two.txt",
    ]
    .into_iter()
    .map(|path| PullRequestFile {
        path: PathBuf::from(path),
        old_path: None,
        status: PullRequestFileStatus::Modified,
        counts: None,
    })
    .collect();

    let entries = app.pull_request_tree_entries();

    assert!(matches!(
        entries.first(),
        Some(PullRequestTreeEntry::Directory { path, label, depth: 0 })
            if path == Path::new("apps/web") && label == "apps/web"
    ));
    assert!(entries.iter().any(|entry| matches!(
        entry,
        PullRequestTreeEntry::Directory { path, label, depth: 1 }
            if path == Path::new("apps/web/modules/marketing/landing")
                && label == "modules/marketing/landing"
    )));

    app.toggle_pull_request_directory(PathBuf::from("apps/web"));
    assert_eq!(app.pull_request_tree_entries().len(), 1);
}

#[test]
fn pull_request_defaults_to_all_files_then_files_tab_restores_it_from_single_file() {
    let mut app = App::new("/tmp/repo", "repo");
    app.view = View::PullRequests;
    app.pull_request_section = PullRequestSection::Files;
    app.focus = Focus::Sidebar;
    app.pull_request = Some(pull_request(8, "Large change", "acme/widget"));
    app.diff_generation = 10;
    let files = ["src/first.rs", "src/second.rs"]
        .into_iter()
        .map(|path| PullRequestFile {
            path: PathBuf::from(path),
            old_path: None,
            status: PullRequestFileStatus::Modified,
            counts: None,
        })
        .collect();
    let now = Instant::now();

    let effects = app.handle_worker_event(
        WorkerEvent::PullRequestIndex {
            generation: 10,
            result: Ok(PullRequestDiffIndex {
                files,
                total_files: 2,
                truncated: false,
            }),
        },
        now,
    );

    assert_eq!(app.pull_request_file_view, PullRequestFileView::AllFiles);
    assert_eq!(app.document.file_count(), 2);
    assert!(app.preview_files_all_collapsed());
    assert!(
        matches!(
            effects.as_slice(),
            [AppEffect::Git(command)] if matches!(
                command.as_ref(),
                WorkerCommand::LoadPullRequestFileBatch {
                    workspace_generation: 10,
                    paths,
                } if paths == &[PathBuf::from("src/first.rs"), PathBuf::from("src/second.rs")]
            )
        ),
        "the whole index is fetched in one batch rather than a file at a time"
    );

    let effects = app.handle_worker_event(
        WorkerEvent::PullRequestDiffBatch {
            workspace_generation: 10,
            result: Ok(vec![
                (
                    PathBuf::from("src/first.rs"),
                    indexed_document(&["src/first.rs"]),
                ),
                (
                    PathBuf::from("src/second.rs"),
                    indexed_document(&["src/second.rs"]),
                ),
            ]),
        },
        now,
    );
    assert!(effects.is_empty(), "no file is left to fetch");
    assert_eq!(app.document.file_count(), 2);

    app.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE), now);
    assert_eq!(app.pull_request_file_cursor, 1);
    assert_eq!(app.pull_request_file_view, PullRequestFileView::SingleFile);
    let (effects, _) = app.tick(now + PREVIEW_DEBOUNCE);
    assert!(
        effects.is_empty(),
        "a prefetched file opens without another Git round trip"
    );
    assert_eq!(app.document.file_count(), 1);
    assert!(!app.preview_files_collapsible());

    app.geometry.sidebar = Rect::new(0, 0, 20, 10);
    app.geometry.sidebar_hits = vec![SidebarHitArea {
        area: Rect::new(1, 1, 8, 1),
        target: SidebarHit::PullRequestFiles,
    }];
    let effects = app.handle_mouse(
        MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column: 2,
            row: 1,
            modifiers: KeyModifiers::NONE,
        },
        now,
    );

    assert!(effects.is_empty(), "the prefetched first file is cached");
    assert_eq!(app.pull_request_file_view, PullRequestFileView::AllFiles);
    assert_eq!(app.document.file_count(), 2);
    assert!(app.preview_files_all_collapsed());
}

#[test]
fn sidebar_wheel_scroll_pans_without_moving_the_selection() {
    let mut app = App::new("/tmp/repo", "repo");
    app.geometry.sidebar = Rect::new(0, 0, 30, 10);
    app.sidebar_viewport(0, 5, 40);
    let cursor_before = app.history_cursor;

    let effects = app.handle_mouse(
        MouseEvent {
            kind: MouseEventKind::ScrollDown,
            column: 2,
            row: 2,
            modifiers: KeyModifiers::NONE,
        },
        Instant::now(),
    );
    assert!(effects.is_empty(), "panning requests no preview");
    assert_eq!(app.history_cursor, cursor_before, "the selection stays put");
    assert_eq!(app.sidebar_offset, 2, "the viewport pans");
    assert!(app.sidebar_free_scroll, "the window detaches");

    app.sidebar_viewport(0, 5, 40);
    assert_eq!(
        app.sidebar_offset, 2,
        "an unmoved selection does not snap the window back"
    );

    app.sidebar_viewport(1, 5, 40);
    assert!(!app.sidebar_free_scroll, "a selection change reattaches");
    assert_eq!(app.sidebar_offset, 1, "the window follows the selection");
}