Files
galaxy/crates/integration/src/test/code_review.rs
T

653 lines
27 KiB
Rust

use std::fs;
use std::path::{Path, PathBuf};
use std::time::Duration;
use command::blocking::Command;
use warp::features::FeatureFlag;
use warp::integration_testing::code_review::{
assert_code_review_anchor, assert_code_review_line_text, assert_code_review_loaded,
assert_code_review_scroll_region, scroll_code_review_to_deleted_range,
scroll_code_review_to_footer, scroll_code_review_to_header, scroll_code_review_to_line,
ScrollRegion,
};
use warp::integration_testing::terminal::wait_until_bootstrapped_single_pane_for_tab;
use warp::integration_testing::view_getters::{single_terminal_view_for_tab, workspace_view};
use warp::workspace::WorkspaceAction;
use galaxyui_core::integration::{AssertionCallback, TestStep};
use galaxyui_core::{async_assert, App, WindowId};
use super::new_builder;
use crate::util::write_all_rc_files_for_test;
use crate::Builder;
const TEST_FILE_NAME: &str = "scroll_target.txt";
const TARGET_LINE_NUMBER: usize = 70;
const INSERT_ABOVE_LINE_NUMBER: usize = 15;
const INSERT_BELOW_LINE_NUMBER: usize = 250;
const INSERTED_LINE_COUNT: usize = 10;
const TOTAL_LINE_COUNT: usize = 400;
fn base_line_text(line_number: usize) -> String {
format!("line {line_number:03}")
}
fn modified_line_text(line_number: usize) -> String {
format!("line {line_number:03} modified")
}
fn initial_committed_contents() -> String {
(1..=TOTAL_LINE_COUNT)
.map(|line_number| format!("{}\n", base_line_text(line_number)))
.collect()
}
fn initial_diff_contents() -> String {
(1..=TOTAL_LINE_COUNT)
.map(|line_number| {
let line_text =
if (10..=80).contains(&line_number) || (200..=300).contains(&line_number) {
modified_line_text(line_number)
} else {
base_line_text(line_number)
};
format!("{line_text}\n")
})
.collect()
}
fn inserted_lines(prefix: &str) -> Vec<String> {
(1..=INSERTED_LINE_COUNT)
.map(|index| format!("{prefix} inserted {index:02}"))
.collect()
}
fn insert_lines(path: &Path, before_line_number: usize, new_lines: &[String]) {
let contents = fs::read_to_string(path).expect("should read test file");
let mut lines: Vec<String> = contents.lines().map(ToOwned::to_owned).collect();
let insert_index = before_line_number.saturating_sub(1);
lines.splice(insert_index..insert_index, new_lines.iter().cloned());
fs::write(path, format!("{}\n", lines.join("\n"))).expect("should rewrite test file");
}
fn run_git(test_dir: &Path, args: &[&str]) {
let status = Command::new("git")
.args(args)
.current_dir(test_dir)
.status()
.expect("git command should run");
assert!(status.success(), "git {:?} should succeed", args);
}
fn open_code_review_panel(app: &mut App, window_id: WindowId) {
let workspace = workspace_view(app, window_id);
app.update(|ctx| {
ctx.dispatch_typed_action_for_view(
window_id,
workspace.id(),
&WorkspaceAction::ToggleRightPanel,
);
});
}
fn assert_repo_detected() -> AssertionCallback {
Box::new(|app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
terminal_view.read(app, |terminal_view, _ctx| {
async_assert!(
terminal_view.current_repo_path().is_some(),
"expected the active terminal to detect a git repository"
)
})
})
}
fn scroll_code_review_to_target_line() -> TestStep {
scroll_code_review_to_line(TEST_FILE_NAME, TARGET_LINE_NUMBER)
.set_timeout(Duration::from_secs(10))
.set_retries(2)
.add_assertion(assert_code_review_anchor(
TEST_FILE_NAME,
modified_line_text(TARGET_LINE_NUMBER),
Some(TARGET_LINE_NUMBER),
))
// Allow the scroll debounce (150ms) to fire so that the stored
// scroll context is captured before the next step mutates the file.
.set_post_step_pause(Duration::from_millis(250))
}
fn mutate_test_file(before_line_number: usize, prefix: &'static str) -> TestStep {
TestStep::new(&format!(
"Insert {INSERTED_LINE_COUNT} lines at {before_line_number}"
))
.with_action(move |app, window_id, _| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
let cwd = terminal_view
.read(app, |terminal_view, _ctx| terminal_view.pwd())
.expect("terminal should expose current working directory");
let file_path = PathBuf::from(cwd).join(TEST_FILE_NAME);
let new_lines = inserted_lines(prefix);
insert_lines(&file_path, before_line_number, &new_lines);
})
.set_post_step_pause(Duration::from_millis(250))
}
fn code_review_scroll_anchor_builder(
insertion_line_number: usize,
insertion_prefix: &'static str,
) -> Builder {
FeatureFlag::CodeReviewScrollPreservation.set_enabled(true);
FeatureFlag::IncrementalAutoReload.set_enabled(true);
let inserted_line_text = inserted_lines(insertion_prefix)
.into_iter()
.next()
.expect("inserted lines should not be empty");
new_builder()
.use_tmp_filesystem_for_test_root_directory()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let repo_dir = test_dir.join("repo");
fs::create_dir_all(&repo_dir).expect("should create repo subdirectory");
let repo_dir_string = repo_dir
.to_str()
.expect("repo directory should be valid utf-8");
write_all_rc_files_for_test(&test_dir, format!("cd {repo_dir_string}"));
fs::write(repo_dir.join(TEST_FILE_NAME), initial_committed_contents())
.expect("should write initial committed contents");
run_git(&repo_dir, &["init", "-b", "main"]);
run_git(&repo_dir, &["config", "user.email", "test@example.com"]);
run_git(&repo_dir, &["config", "user.name", "Warp Integration Test"]);
run_git(&repo_dir, &["add", TEST_FILE_NAME]);
run_git(&repo_dir, &["commit", "-m", "Initial commit"]);
fs::write(repo_dir.join(TEST_FILE_NAME), initial_diff_contents())
.expect("should write initial diff contents");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Wait for the terminal to detect the git repository")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_repo_detected()),
)
.with_step(
TestStep::new("Open the code review panel")
.with_action(|app, window_id, _| open_code_review_panel(app, window_id)),
)
.with_step(
TestStep::new("Wait for the code review panel to load file diffs")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_loaded()),
)
.with_step(scroll_code_review_to_target_line())
.with_step(mutate_test_file(insertion_line_number, insertion_prefix))
.with_step(
TestStep::new("Wait for code review to reflect the inserted lines")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_line_text(
TEST_FILE_NAME,
insertion_line_number,
inserted_line_text,
)),
)
.with_step(
TestStep::new("Wait for code review to preserve the visible anchor text")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_anchor(
TEST_FILE_NAME,
modified_line_text(TARGET_LINE_NUMBER),
None,
)),
)
}
pub fn test_code_review_scroll_anchor_preserved_when_inserting_above() -> Builder {
code_review_scroll_anchor_builder(INSERT_ABOVE_LINE_NUMBER, "above")
}
pub fn test_code_review_scroll_anchor_unchanged_when_inserting_below() -> Builder {
code_review_scroll_anchor_builder(INSERT_BELOW_LINE_NUMBER, "below")
}
// --- Multi-file test ---
// Tests that scroll preservation works when scrolled to the second file in the
// code review list. This exercises the adjustment callback returning an
// item-relative offset (not absolute), which only matters for index > 0.
const SECOND_FILE_NAME: &str = "second_file.txt";
const FIRST_FILE_NAME: &str = "first_file.txt";
const MULTI_FILE_TARGET_LINE: usize = 70;
const MULTI_FILE_INSERT_LINE: usize = 15;
fn multi_file_base_line(file_prefix: &str, line_number: usize) -> String {
format!("{file_prefix} line {line_number:03}")
}
fn multi_file_modified_line(file_prefix: &str, line_number: usize) -> String {
format!("{file_prefix} line {line_number:03} modified")
}
fn multi_file_committed_contents(file_prefix: &str) -> String {
(1..=TOTAL_LINE_COUNT)
.map(|n| format!("{}\n", multi_file_base_line(file_prefix, n)))
.collect()
}
fn multi_file_diff_contents(file_prefix: &str) -> String {
(1..=TOTAL_LINE_COUNT)
.map(|n| {
let text = if (10..=80).contains(&n) || (200..=300).contains(&n) {
multi_file_modified_line(file_prefix, n)
} else {
multi_file_base_line(file_prefix, n)
};
format!("{text}\n")
})
.collect()
}
fn mutate_named_file(
file_name: &'static str,
before_line_number: usize,
prefix: &'static str,
) -> TestStep {
TestStep::new(&format!(
"Insert {INSERTED_LINE_COUNT} lines at {before_line_number} in {file_name}"
))
.with_action(move |app, window_id, _| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
let cwd = terminal_view
.read(app, |terminal_view, _ctx| terminal_view.pwd())
.expect("terminal should expose current working directory");
let file_path = PathBuf::from(cwd).join(file_name);
let new_lines = inserted_lines(prefix);
insert_lines(&file_path, before_line_number, &new_lines);
})
.set_post_step_pause(Duration::from_millis(250))
}
// --- Deleted range test ---
// Tests that scroll preservation works when scrolled to a deleted (removed) line
// region. This exercises the RemovedLine variant of RelocatableScrollContext.
const DELETED_RANGE_START: usize = 61;
const DELETED_RANGE_END: usize = 80;
/// Current buffer line just before the deleted range. The temporary blocks
/// for deleted lines 61-80 appear immediately after this line in the diff.
const DELETED_RANGE_NEAR_LINE: usize = 60;
fn deleted_range_diff_contents() -> String {
(1..=TOTAL_LINE_COUNT)
.filter(|&n| !(DELETED_RANGE_START..=DELETED_RANGE_END).contains(&n))
.map(|n| {
let text = if (200..=300).contains(&n) {
modified_line_text(n)
} else {
base_line_text(n)
};
format!("{text}\n")
})
.collect()
}
pub fn test_code_review_scroll_preserved_deleted_range() -> Builder {
FeatureFlag::CodeReviewScrollPreservation.set_enabled(true);
FeatureFlag::IncrementalAutoReload.set_enabled(true);
let inserted_line_text = inserted_lines("above")
.into_iter()
.next()
.expect("inserted lines should not be empty");
new_builder()
.use_tmp_filesystem_for_test_root_directory()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let repo_dir = test_dir.join("repo");
fs::create_dir_all(&repo_dir).expect("should create repo subdirectory");
let repo_dir_string = repo_dir
.to_str()
.expect("repo directory should be valid utf-8");
write_all_rc_files_for_test(&test_dir, format!("cd {repo_dir_string}"));
fs::write(repo_dir.join(TEST_FILE_NAME), initial_committed_contents())
.expect("should write initial committed contents");
run_git(&repo_dir, &["init", "-b", "main"]);
run_git(&repo_dir, &["config", "user.email", "test@example.com"]);
run_git(&repo_dir, &["config", "user.name", "Warp Integration Test"]);
run_git(&repo_dir, &["add", TEST_FILE_NAME]);
run_git(&repo_dir, &["commit", "-m", "Initial commit"]);
// Write diff contents that DELETE lines 61-80
fs::write(repo_dir.join(TEST_FILE_NAME), deleted_range_diff_contents())
.expect("should write deleted range diff contents");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Wait for the terminal to detect the git repository")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_repo_detected()),
)
.with_step(
TestStep::new("Open the code review panel")
.with_action(|app, window_id, _| open_code_review_panel(app, window_id)),
)
.with_step(
TestStep::new("Wait for the code review panel to load file diffs")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_loaded()),
)
.with_step(
scroll_code_review_to_deleted_range(TEST_FILE_NAME, DELETED_RANGE_NEAR_LINE)
.set_timeout(Duration::from_secs(10))
.set_retries(2)
.add_assertion(assert_code_review_scroll_region(ScrollRegion::RemovedLine))
.set_post_step_pause(Duration::from_millis(250)),
)
.with_step(mutate_test_file(INSERT_ABOVE_LINE_NUMBER, "above"))
.with_step(
TestStep::new("Wait for code review to reflect the inserted lines")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_line_text(
TEST_FILE_NAME,
INSERT_ABOVE_LINE_NUMBER,
inserted_line_text,
))
// Allow time for the asynchronous diff recomputation to complete.
// Without this, the assertion below may pass against the stale
// (pre-recompute) layout where temporary blocks haven't moved.
.set_post_step_pause(Duration::from_millis(1000)),
)
.with_step(
TestStep::new("Assert scroll is still in the deleted range after preservation")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_scroll_region(ScrollRegion::RemovedLine)),
)
}
// --- Header range test ---
// Tests that scroll preservation works when scrolled to the file header region.
// This exercises the Header variant of RelocatableScrollContext.
pub fn test_code_review_scroll_preserved_header_range() -> Builder {
FeatureFlag::CodeReviewScrollPreservation.set_enabled(true);
FeatureFlag::IncrementalAutoReload.set_enabled(true);
let inserted_line_text = inserted_lines("above")
.into_iter()
.next()
.expect("inserted lines should not be empty");
new_builder()
.use_tmp_filesystem_for_test_root_directory()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let repo_dir = test_dir.join("repo");
fs::create_dir_all(&repo_dir).expect("should create repo subdirectory");
let repo_dir_string = repo_dir
.to_str()
.expect("repo directory should be valid utf-8");
write_all_rc_files_for_test(&test_dir, format!("cd {repo_dir_string}"));
fs::write(repo_dir.join(TEST_FILE_NAME), initial_committed_contents())
.expect("should write initial committed contents");
run_git(&repo_dir, &["init", "-b", "main"]);
run_git(&repo_dir, &["config", "user.email", "test@example.com"]);
run_git(&repo_dir, &["config", "user.name", "Warp Integration Test"]);
run_git(&repo_dir, &["add", TEST_FILE_NAME]);
run_git(&repo_dir, &["commit", "-m", "Initial commit"]);
fs::write(repo_dir.join(TEST_FILE_NAME), initial_diff_contents())
.expect("should write initial diff contents");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Wait for the terminal to detect the git repository")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_repo_detected()),
)
.with_step(
TestStep::new("Open the code review panel")
.with_action(|app, window_id, _| open_code_review_panel(app, window_id)),
)
.with_step(
TestStep::new("Wait for the code review panel to load file diffs")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_loaded()),
)
.with_step(
scroll_code_review_to_header(TEST_FILE_NAME)
.set_timeout(Duration::from_secs(10))
.set_retries(2)
.add_assertion(assert_code_review_scroll_region(ScrollRegion::Header))
.set_post_step_pause(Duration::from_millis(250)),
)
.with_step(mutate_test_file(INSERT_ABOVE_LINE_NUMBER, "above"))
.with_step(
TestStep::new("Wait for code review to reflect the inserted lines")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_line_text(
TEST_FILE_NAME,
INSERT_ABOVE_LINE_NUMBER,
inserted_line_text,
)),
)
.with_step(
TestStep::new("Assert scroll is still in the header region after preservation")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_scroll_region(ScrollRegion::Header)),
)
}
// --- Footer range test ---
// Tests that scroll preservation works when scrolled past the editor content
// into the footer region. This exercises the Footer variant of RelocatableScrollContext.
//
// The footer region of a file is only reachable when there is a sufficiently
// tall file below it in the list; otherwise the list's max-scroll clamping
// prevents scrolling past the editor content. This test reuses the multi-file
// helpers (FIRST_FILE_NAME / SECOND_FILE_NAME) so that first_file.txt (index 0)
// has second_file.txt below it, making the footer reachable.
pub fn test_code_review_scroll_preserved_footer_range() -> Builder {
FeatureFlag::CodeReviewScrollPreservation.set_enabled(true);
FeatureFlag::IncrementalAutoReload.set_enabled(true);
let inserted_line_text = inserted_lines("first")
.into_iter()
.next()
.expect("inserted lines should not be empty");
new_builder()
.use_tmp_filesystem_for_test_root_directory()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let repo_dir = test_dir.join("repo");
fs::create_dir_all(&repo_dir).expect("should create repo subdirectory");
let repo_dir_string = repo_dir
.to_str()
.expect("repo directory should be valid utf-8");
write_all_rc_files_for_test(&test_dir, format!("cd {repo_dir_string}"));
// Two files: first_file.txt at index 0, second_file.txt at index 1.
// We scroll to the footer of the first file; the second file
// provides enough total list height so the footer is reachable.
fs::write(
repo_dir.join(FIRST_FILE_NAME),
multi_file_committed_contents("first"),
)
.expect("should write first file committed contents");
fs::write(
repo_dir.join(SECOND_FILE_NAME),
multi_file_committed_contents("second"),
)
.expect("should write second file committed contents");
run_git(&repo_dir, &["init", "-b", "main"]);
run_git(&repo_dir, &["config", "user.email", "test@example.com"]);
run_git(&repo_dir, &["config", "user.name", "Warp Integration Test"]);
run_git(&repo_dir, &["add", FIRST_FILE_NAME, SECOND_FILE_NAME]);
run_git(&repo_dir, &["commit", "-m", "Initial commit"]);
fs::write(
repo_dir.join(FIRST_FILE_NAME),
multi_file_diff_contents("first"),
)
.expect("should write first file diff contents");
fs::write(
repo_dir.join(SECOND_FILE_NAME),
multi_file_diff_contents("second"),
)
.expect("should write second file diff contents");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Wait for the terminal to detect the git repository")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_repo_detected()),
)
.with_step(
TestStep::new("Open the code review panel")
.with_action(|app, window_id, _| open_code_review_panel(app, window_id)),
)
.with_step(
TestStep::new("Wait for the code review panel to load file diffs")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_loaded()),
)
.with_step(
scroll_code_review_to_footer(FIRST_FILE_NAME)
.set_timeout(Duration::from_secs(10))
.set_retries(2)
.add_assertion(assert_code_review_scroll_region(ScrollRegion::Footer))
.set_post_step_pause(Duration::from_millis(250)),
)
.with_step(mutate_named_file(
FIRST_FILE_NAME,
MULTI_FILE_INSERT_LINE,
"first",
))
.with_step(
TestStep::new("Wait for code review to reflect the inserted lines")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_line_text(
FIRST_FILE_NAME,
MULTI_FILE_INSERT_LINE,
inserted_line_text,
)),
)
.with_step(
TestStep::new("Assert scroll is still in the footer region after preservation")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_scroll_region(ScrollRegion::Footer)),
)
}
pub fn test_code_review_scroll_preserved_second_file() -> Builder {
FeatureFlag::CodeReviewScrollPreservation.set_enabled(true);
FeatureFlag::IncrementalAutoReload.set_enabled(true);
let inserted_line_text = inserted_lines("second")
.into_iter()
.next()
.expect("inserted lines should not be empty");
new_builder()
.use_tmp_filesystem_for_test_root_directory()
.with_setup(|utils| {
let test_dir = utils.test_dir();
let repo_dir = test_dir.join("repo");
fs::create_dir_all(&repo_dir).expect("should create repo subdirectory");
let repo_dir_string = repo_dir
.to_str()
.expect("repo directory should be valid utf-8");
write_all_rc_files_for_test(&test_dir, format!("cd {repo_dir_string}"));
// Create and commit two files. File names sort alphabetically so
// first_file.txt appears at index 0 and second_file.txt at index 1.
fs::write(
repo_dir.join(FIRST_FILE_NAME),
multi_file_committed_contents("first"),
)
.expect("should write first file committed contents");
fs::write(
repo_dir.join(SECOND_FILE_NAME),
multi_file_committed_contents("second"),
)
.expect("should write second file committed contents");
run_git(&repo_dir, &["init", "-b", "main"]);
run_git(&repo_dir, &["config", "user.email", "test@example.com"]);
run_git(&repo_dir, &["config", "user.name", "Warp Integration Test"]);
run_git(&repo_dir, &["add", FIRST_FILE_NAME, SECOND_FILE_NAME]);
run_git(&repo_dir, &["commit", "-m", "Initial commit"]);
// Write modified versions to create diffs in both files
fs::write(
repo_dir.join(FIRST_FILE_NAME),
multi_file_diff_contents("first"),
)
.expect("should write first file diff contents");
fs::write(
repo_dir.join(SECOND_FILE_NAME),
multi_file_diff_contents("second"),
)
.expect("should write second file diff contents");
})
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
.with_step(
TestStep::new("Wait for the terminal to detect the git repository")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_repo_detected()),
)
.with_step(
TestStep::new("Open the code review panel")
.with_action(|app, window_id, _| open_code_review_panel(app, window_id)),
)
.with_step(
TestStep::new("Wait for the code review panel to load file diffs")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_loaded()),
)
// Scroll to a target line in the SECOND file (index 1)
.with_step(
scroll_code_review_to_line(SECOND_FILE_NAME, MULTI_FILE_TARGET_LINE)
.set_timeout(Duration::from_secs(10))
.set_retries(2)
.add_assertion(assert_code_review_anchor(
SECOND_FILE_NAME,
multi_file_modified_line("second", MULTI_FILE_TARGET_LINE),
Some(MULTI_FILE_TARGET_LINE),
))
.set_post_step_pause(Duration::from_millis(250)),
)
// Insert lines above the target in the second file
.with_step(mutate_named_file(
SECOND_FILE_NAME,
MULTI_FILE_INSERT_LINE,
"second",
))
.with_step(
TestStep::new("Wait for code review to reflect the inserted lines in second file")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_line_text(
SECOND_FILE_NAME,
MULTI_FILE_INSERT_LINE,
inserted_line_text,
)),
)
.with_step(
TestStep::new("Wait for code review to preserve the visible anchor in second file")
.set_timeout(Duration::from_secs(20))
.add_assertion(assert_code_review_anchor(
SECOND_FILE_NAME,
multi_file_modified_line("second", MULTI_FILE_TARGET_LINE),
None,
)),
)
}