196 lines
6.9 KiB
Rust
196 lines
6.9 KiB
Rust
//! End-to-end tests for the `jmove` CLI against the TypeScript fixtures.
|
|
|
|
mod common;
|
|
|
|
use common::{copy_fixture, fixture_dir, in_root, jmove, read};
|
|
use predicates::prelude::*;
|
|
use tempfile::TempDir;
|
|
|
|
/// Convenience wrapper: a TypeScript fixture by name.
|
|
fn fixture(name: &str) -> TempDir {
|
|
copy_fixture("typescript", name)
|
|
}
|
|
|
|
#[test]
|
|
fn mv_dry_run_prints_diff_and_leaves_disk_untouched() {
|
|
let tmp = fixture("basic");
|
|
jmove(&tmp, &["mv", "lib/sum.ts", "utils/sum.ts", "--dry-run"])
|
|
.success()
|
|
.stdout(predicate::str::contains("./lib/sum"))
|
|
.stdout(predicate::str::contains("./utils/sum"));
|
|
assert!(
|
|
in_root(tmp.path(), "lib/sum.ts").is_file(),
|
|
"source untouched"
|
|
);
|
|
assert!(
|
|
!in_root(tmp.path(), "utils/sum.ts").exists(),
|
|
"target not created"
|
|
);
|
|
assert_eq!(
|
|
read(&in_root(tmp.path(), "app.ts")),
|
|
read(&fixture_dir("typescript", "basic").join("app.ts")),
|
|
"importer untouched"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mv_rewrites_importer_and_moves_the_file() {
|
|
let tmp = fixture("basic");
|
|
jmove(&tmp, &["mv", "lib/sum.ts", "utils/sum.ts"])
|
|
.success()
|
|
.stdout(predicate::str::contains("moved lib/sum.ts -> utils/sum.ts"))
|
|
.stdout(predicate::str::contains("1 import"))
|
|
.stdout(predicate::str::contains("1 file"));
|
|
assert!(
|
|
!in_root(tmp.path(), "lib/sum.ts").exists(),
|
|
"source is gone"
|
|
);
|
|
assert!(
|
|
in_root(tmp.path(), "utils/sum.ts").is_file(),
|
|
"target exists"
|
|
);
|
|
assert!(
|
|
read(&in_root(tmp.path(), "app.ts")).contains("./utils/sum"),
|
|
"importer updated"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mv_updates_barrel_and_never_touches_node_modules() {
|
|
let tmp = fixture("normal");
|
|
jmove(&tmp, &["mv", "src/impl/core.ts", "src/impl/calc.ts"]).success();
|
|
assert!(
|
|
read(&in_root(tmp.path(), "src/impl/index.ts")).contains("./calc"),
|
|
"barrel re-export updated"
|
|
);
|
|
let app = read(&in_root(tmp.path(), "src/app.ts"));
|
|
assert!(!app.contains("./impl/core"), "side-effect import updated");
|
|
assert!(app.contains("./utils/logger"), "unrelated import preserved");
|
|
let vendored = read(&in_root(tmp.path(), "node_modules/legacy-lib/index.ts"));
|
|
assert!(
|
|
vendored.contains("../../src/impl/core"),
|
|
"ignored directories must never be rewritten"
|
|
);
|
|
jmove(&tmp, &["check"]).success();
|
|
}
|
|
|
|
#[test]
|
|
fn mv_rewrites_only_the_specifier_line_in_multi_line_imports() {
|
|
let tmp = fixture("complex");
|
|
let view = in_root(tmp.path(), "src/ui/deep/nested/view.ts");
|
|
let before: Vec<String> = read(&view).lines().map(str::to_owned).collect();
|
|
|
|
jmove(&tmp, &["mv", "src/util/text.ts", "src/core/text.ts"]).success();
|
|
|
|
// Second importer at another depth: the messy `./util/../util/text`
|
|
// specifier resolves to the moved file and must be rewritten as well.
|
|
let config = read(&in_root(tmp.path(), "src/config.ts"));
|
|
assert!(
|
|
config.contains("./core/text") && !config.contains("../util"),
|
|
"messy relative specifier updated: {config}"
|
|
);
|
|
let after: Vec<String> = read(&view).lines().map(str::to_owned).collect();
|
|
assert_eq!(before.len(), after.len(), "import layout preserved");
|
|
assert_eq!(before[0], after[0], "opening line is byte-identical");
|
|
assert_eq!(before[1], after[1], "binding line is byte-identical");
|
|
assert_eq!(before[2], "} from \"../../../util/text\";");
|
|
assert_eq!(
|
|
after[2], "} from \"../../../core/text\";",
|
|
"only the specifier line changed"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn check_passes_on_clean_and_fails_on_broken_fixture() {
|
|
let clean = fixture("basic");
|
|
jmove(&clean, &["check"])
|
|
.success()
|
|
.stdout(predicate::str::contains("no broken imports"));
|
|
|
|
let messy = fixture("complex");
|
|
jmove(&messy, &["check"])
|
|
.code(2)
|
|
.stdout(predicate::str::contains(
|
|
"src/broken.ts:4: cannot resolve './gone'",
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn check_json_reports_broken_import_payload() {
|
|
let tmp = fixture("complex");
|
|
jmove(&tmp, &["check", "--json"]).code(2).stdout(
|
|
predicate::str::contains("\"status\": \"ok\"")
|
|
.and(predicate::str::contains("\"operation\": \"check\""))
|
|
.and(predicate::str::contains("\"file\": \"src/broken.ts\""))
|
|
.and(predicate::str::contains("\"line\": 4"))
|
|
.and(predicate::str::contains("\"import\": \"./gone\""))
|
|
.and(predicate::str::contains("\"reason\": \"file_not_found\""))
|
|
.and(predicate::str::contains("\"total\": 1")),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mv_json_happy_path_reports_changed_files() {
|
|
let tmp = fixture("basic");
|
|
jmove(&tmp, &["mv", "lib/sum.ts", "utils/sum.ts", "--json"])
|
|
.success()
|
|
.stdout(
|
|
predicate::str::contains("\"status\": \"ok\"")
|
|
.and(predicate::str::contains("\"operation\": \"mv\""))
|
|
.and(predicate::str::contains("\"source\": \"lib/sum.ts\""))
|
|
.and(predicate::str::contains("\"target\": \"utils/sum.ts\""))
|
|
.and(predicate::str::contains("\"path\": \"app.ts\""))
|
|
.and(predicate::str::contains("\"line\": 1"))
|
|
.and(predicate::str::contains("\"old\": \"./lib/sum\""))
|
|
.and(predicate::str::contains("\"new\": \"./utils/sum\""))
|
|
.and(predicate::str::contains("\"moved\": 1"))
|
|
.and(predicate::str::contains("\"updated_imports\": 1")),
|
|
);
|
|
assert!(in_root(tmp.path(), "utils/sum.ts").is_file());
|
|
}
|
|
|
|
#[test]
|
|
fn mv_json_dry_run_reports_preview_payload() {
|
|
let tmp = fixture("basic");
|
|
jmove(
|
|
&tmp,
|
|
&["mv", "lib/sum.ts", "utils/sum.ts", "--dry-run", "--json"],
|
|
)
|
|
.success()
|
|
.stdout(
|
|
predicate::str::contains("\"status\": \"dry_run\"")
|
|
.and(predicate::str::contains("\"would_move\": \"lib/sum.ts\""))
|
|
.and(predicate::str::contains("\"would_update\": 1"))
|
|
.and(predicate::str::contains("\"affected_files\""))
|
|
.and(predicate::str::contains("\"app.ts\"")),
|
|
);
|
|
assert!(
|
|
in_root(tmp.path(), "lib/sum.ts").is_file(),
|
|
"dry-run writes nothing"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mv_json_reports_target_exists_error_shape() {
|
|
let tmp = fixture("basic");
|
|
jmove(&tmp, &["mv", "app.ts", "lib/sum.ts", "--json"])
|
|
.code(1)
|
|
.stdout(
|
|
predicate::str::contains("\"status\": \"error\"")
|
|
.and(predicate::str::contains("\"operation\": \"mv\""))
|
|
.and(predicate::str::contains("\"code\": \"TARGET_EXISTS\""))
|
|
.and(predicate::str::contains("\"hint\"")),
|
|
);
|
|
assert!(
|
|
read(&in_root(tmp.path(), "lib/sum.ts")).contains("export function sum"),
|
|
"rejected move changes nothing"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mv_reports_source_not_found() {
|
|
let tmp = fixture("basic");
|
|
jmove(&tmp, &["mv", "lib/nope.ts", "utils/nope.ts", "--json"])
|
|
.code(1)
|
|
.stdout(predicate::str::contains("\"code\": \"SOURCE_NOT_FOUND\""));
|
|
}
|